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

Commit a1808e0

Browse files
committed
Add creation of avro-schema from query with
multi-head connections, Unions, Maps and directives Also changed GraphQL Map type: now it has subType = Map and name = values.type .. '_Map' (Example: 'String_Map') This is to avoid name clash. Closes #200, closes #198, closes #197, closes #85, closes #85
1 parent 79f01fc commit a1808e0

14 files changed

+1834
-229
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,10 @@ git clone https://github.com/tarantool/graphql.git
368368
git submodule update --recursive --init
369369
make test
370370
```
371+
To run specific test:
372+
```
373+
TEST_RUN_TESTS=common/mutation make test
374+
```
371375

372376
## Requirements
373377

graphql/convert_schema/resolve.lua

+19
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@ function resolve.gen_resolve_function_multihead(collection_name, connection,
190190
end
191191

192192
return function(parent, _, info)
193+
-- If parent object does not have all source_fields (for any of
194+
-- variants) non-nill then we do not resolve variant and just return
195+
-- box.NULL
196+
for _, variant in ipairs(c.variants) do
197+
for _, p in ipairs(variant.parts) do
198+
local found = false
199+
for field_name, value in pairs(parent) do
200+
if field_name == p.source_field and value ~= nil then
201+
found = true
202+
break
203+
end
204+
end
205+
206+
if not found then
207+
return box.NULL, nil
208+
end
209+
end
210+
end
211+
193212
local v, variant_num, box_field_name = resolve_variant(parent)
194213
local destination_type = union_types[variant_num]
195214

graphql/convert_schema/types.lua

+9-3
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,16 @@ function types.convert(state, avro_schema, opts)
448448
'got %s (avro_schema %s)'):format(type(avro_schema.values),
449449
json.encode(avro_schema)))
450450

451-
-- validate avro schema format inside 'values'
452-
types.convert(state, avro_schema.values, {context = context})
451+
local converted_values = types.convert(state, avro_schema.values,
452+
{context = context})
453+
454+
local true_values_type = converted_values
455+
while true_values_type.__type == 'NonNull' do
456+
true_values_type = true_values_type.ofType
457+
end
458+
local map_name = true_values_type.name .. "_Map"
453459

454-
local res = core_types.map
460+
local res = core_types.map({values = converted_values, name = map_name })
455461
return avro_t == 'map' and core_types.nonNull(res) or res
456462
elseif avro_t == 'union' then
457463
return union.convert(avro_schema, {

graphql/convert_schema/union.lua

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ local function box_type(type_to_box, box_field_name, opts)
3838
-- for the argument type to avoid 'Encountered multiple types' error. See
3939
-- also the comment in @{types.box_collection_type}.
4040
local box_name = (gql_true_type.name or gql_true_type.__type) .. '_box'
41+
if (gql_true_type.__type == "Scalar" and gql_true_type.subtype == 'Map') then
42+
box_name = 'Map_box'
43+
end
4144
if gen_argument then
4245
box_name = helpers.full_name(box_name, context)
4346
else

graphql/core/types.lua

+18-11
Original file line numberDiff line numberDiff line change
@@ -191,17 +191,24 @@ function types.union(config)
191191
return instance
192192
end
193193

194-
types.map = types.scalar({
195-
name = 'Map',
196-
description = 'Map is a dictionary with string keys and values of ' ..
197-
'arbitrary but same among all values type',
198-
serialize = function(value) return value end,
199-
parseValue = function(value) return value end,
200-
parseLiteral = function(node)
201-
error('Literal parsing is implemented in util.coerceValue; ' ..
202-
'we should not go here')
203-
end,
204-
})
194+
function types.map(config)
195+
local instance = {
196+
__type = 'Scalar',
197+
subtype = 'Map',
198+
name = config.name,
199+
description = 'Map is a dictionary with string keys and values of ' ..
200+
'arbitrary but same among all values type',
201+
serialize = function(value) return value end,
202+
parseValue = function(value) return value end,
203+
parseLiteral = function(node)
204+
error('Literal parsing is implemented in util.coerceValue; ' ..
205+
'we should not go here')
206+
end,
207+
values = config.values,
208+
}
209+
210+
return instance
211+
end
205212

206213
function types.inputObject(config)
207214
assert(type(config.name) == 'string', 'type name must be provided as a string')

graphql/impl.lua

+3-3
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ end
7373
--- @treturn table result of the operation
7474
local function compile_and_execute(state, query, variables, operation_name,
7575
opts)
76-
assert(type(state) == 'table', 'use :gql_execute(...) instead of ' ..
77-
'.execute(...)')
76+
assert(type(state) == 'table', 'use :compile_and_execute(...) ' ..
77+
'instead of .compile_and_execute(...)')
7878
assert(state.schema ~= nil, 'have not compiled schema')
7979
check(query, 'query', 'string')
8080
check(variables, 'variables', 'table', 'nil')
@@ -103,7 +103,7 @@ end
103103
--- @treturn table compiled query with `execute` and `avro_schema` functions
104104
local function gql_compile(state, query, opts)
105105
assert(type(state) == 'table' and type(query) == 'string',
106-
'use :validate(...) instead of .validate(...)')
106+
'use :gql_compile(...) instead of .gql_compile(...)')
107107
assert(state.schema ~= nil, 'have not compiled schema')
108108
check(query, 'query', 'string')
109109
check(opts, 'opts', 'table', 'nil')

graphql/query_to_avro.lua

+152-5
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ local introspection = require(path .. '.introspection')
1010
local query_util = require(path .. '.query_util')
1111
local avro_helpers = require('graphql.avro_helpers')
1212
local convert_schema_helpers = require('graphql.convert_schema.helpers')
13+
local utils = require('graphql.utils')
14+
local check = utils.check
1315

1416
-- module functions
1517
local query_to_avro = {}
1618

1719
-- forward declaration
1820
local object_to_avro
21+
local union_to_avro
22+
local map_to_avro
1923

2024
local gql_scalar_to_avro_index = {
2125
String = "string",
@@ -29,7 +33,9 @@ local gql_scalar_to_avro_index = {
2933

3034
local function gql_scalar_to_avro(fieldType)
3135
assert(fieldType.__type == "Scalar", "GraphQL scalar field expected")
32-
assert(fieldType.name ~= "Map", "Map type is not supported")
36+
if (fieldType.subtype == "Map") then
37+
return map_to_avro(fieldType)
38+
end
3339
local result = gql_scalar_to_avro_index[fieldType.name]
3440
assert(result ~= nil, "Unexpected scalar type: " .. fieldType.name)
3541
return result
@@ -45,8 +51,14 @@ end
4551
--- @tparam table context current traversal context, here it just falls to the
4652
--- called functions (internal graphql-lua format)
4753
---
54+
--- @tparam table opts the following options:
55+
---
56+
--- * is_nullable (boolean) whether resulting type should be nullable or not.
57+
--- If passed, then the function will not care if original GraphQL type was
58+
--- nullable or not.
59+
---
4860
--- @treturn table `result` is the resulting avro-schema
49-
local function gql_type_to_avro(fieldType, subSelections, context)
61+
local function gql_type_to_avro(fieldType, subSelections, context, opts)
5062
local fieldTypeName = fieldType.__type
5163
local isNonNull = false
5264

@@ -71,20 +83,115 @@ local function gql_type_to_avro(fieldType, subSelections, context)
7183
result = gql_scalar_to_avro(fieldType)
7284
elseif fieldTypeName == 'Object' then
7385
result = object_to_avro(fieldType, subSelections, context)
74-
elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then
75-
error('Interfaces and Unions are not supported yet')
86+
elseif fieldTypeName == 'Union' then
87+
result = union_to_avro(fieldType, subSelections, context)
88+
elseif fieldTypeName == 'Interface' then
89+
error('Interfaces are not supported yet')
7690
else
7791
error(string.format('Unknown type "%s"', tostring(fieldTypeName)))
7892
end
7993

80-
if not isNonNull then
94+
local opts = opts or {}
95+
opts.is_nullable = opts.is_nullable or false
96+
if not isNonNull or opts.is_nullable then
8197
result = avro_helpers.make_avro_type_nullable(result, {
8298
raise_on_nullable = true,
8399
})
84100
end
85101
return result
86102
end
87103

104+
--- The function converts a GraphQL Map type to avro-schema map type.
105+
map_to_avro = function(mapType)
106+
assert(mapType.values ~= nil, "GraphQL Map type must have 'values' field")
107+
return {
108+
type = "map",
109+
values = gql_type_to_avro(mapType.values),
110+
}
111+
end
112+
113+
--- The function converts a GraphQL Union type to avro-schema type.
114+
---
115+
--- Currently we use GraphQL Unions to implement both multi-head connections
116+
--- and avro-schema unions. The function distinguishes between them relying on
117+
--- 'fieldType.resolveType'. GraphQL Union implementing multi-head
118+
--- connection does not have such field, as it has another mechanism of union
119+
--- type resolving.
120+
---
121+
--- We have to distinguish between these two types of GraphQL Unions because
122+
--- we want to create different avro-schemas for them.
123+
---
124+
--- GraphQL Unions implementing avro-schema unions are to be converted back
125+
--- to avro-schema unions.
126+
---
127+
--- GraphQL Unions implementing multi-head connections are to be converted to
128+
--- avro-schema records. Each field represents one union variant. Variant type
129+
--- name is taken as a field name. Such records must have all fields nullable.
130+
---
131+
--- We convert multi-head Unions to records instead of unions because in case
132+
--- of 1:N connections we would not have valid avro-schema (if use unions).
133+
--- This is because according to avro-schema standard unions may not contain
134+
--- more than one schema with the same type (in case of 1:N multi-head
135+
--- connections we would have more than one 'array' in union)
136+
union_to_avro = function(fieldType, subSelections, context)
137+
assert(fieldType.types ~= nil, "GraphQL Union must have 'types' field")
138+
check(fieldType.types, "fieldType.types", "table")
139+
local is_multihead = (fieldType.resolveType == nil)
140+
local result = {}
141+
142+
if is_multihead then
143+
check(fieldType.name, "fieldType.name", "string")
144+
result = {
145+
type = 'record',
146+
name = fieldType.name,
147+
fields = {}
148+
}
149+
end
150+
151+
for _, box_type in ipairs(fieldType.types) do
152+
-- In GraphQL schema all types in Unions are 'boxed'. Here we
153+
-- 'Unbox' types and selectionSets. More info on 'boxing' can be
154+
-- found at @{convert_schema.types.convert_multihead_connection}
155+
-- and at @{convert_schema.union}.
156+
check(box_type, "box_type", "table")
157+
assert(box_type.__type == "Object", "Box type must be a GraphQL Object")
158+
assert(utils.table_size(box_type.fields) == 1, 'Box Object must ' ..
159+
'have exactly one field')
160+
local type =
161+
box_type.fields[utils.get_keys(box_type.fields)[1]]
162+
163+
local box_sub_selections
164+
for _, s in pairs(subSelections) do
165+
if s.typeCondition.name.value == box_type.name then
166+
box_sub_selections = s
167+
break
168+
end
169+
end
170+
171+
-- We have to extract subSelections from 'box' type.
172+
local type_sub_selections
173+
if box_sub_selections.selectionSet.selections[1].selectionSet ~= nil then
174+
-- Object GraphQL type case.
175+
type_sub_selections =
176+
box_sub_selections.selectionSet.selections[1].selectionSet.selections
177+
else
178+
-- Scalar GraphQL type case.
179+
type_sub_selections = box_sub_selections.selectionSet.selections[1]
180+
end
181+
182+
if is_multihead then
183+
local avro_type = gql_type_to_avro(type.kind,
184+
type_sub_selections, context, {is_nullable = true})
185+
table.insert(result.fields, {name = type.name, type = avro_type})
186+
else
187+
table.insert(result, gql_type_to_avro(type.kind,
188+
type_sub_selections, context))
189+
end
190+
end
191+
192+
return result
193+
end
194+
88195
--- The function converts a single Object field to avro format.
89196
local function field_to_avro(object_type, fields, context)
90197
local firstField = fields[1]
@@ -97,6 +204,46 @@ local function field_to_avro(object_type, fields, context)
97204

98205
local fieldTypeAvro = gql_type_to_avro(fieldType.kind, subSelections,
99206
context)
207+
-- Currently we support only 'include' and 'skip' directives. Both of them
208+
-- affect resulting avro-schema the same way: field with directive becomes
209+
-- nullable, if it's already not. Nullable field does not change.
210+
--
211+
-- If it is a 1:N connection then it's 'array' field becomes 'array*'.
212+
-- If it is avro-schema union, then 'null' will be added to the union
213+
-- types. If there are more then one directive on a field then all works
214+
-- the same way, like it is only one directive. (But we still check all
215+
-- directives to be 'include' or 'skip').
216+
if firstField.directives ~= nil then
217+
for _, d in ipairs(firstField.directives) do
218+
check(d.name, "directive.name", "table")
219+
check(d.arguments, "directive.arguments", "table")
220+
check(d.kind, "directive.kind", "string")
221+
assert(d.kind == "directive")
222+
check(d.name.value, "directive.name.value", "string")
223+
assert(d.name.value == "include" or d.name.value == "skip",
224+
"Only 'include' and 'skip' directives are supported for now")
225+
end
226+
if type(fieldTypeAvro) == "string" then
227+
if string.sub(fieldTypeAvro, -1) ~= '*' then
228+
fieldTypeAvro = fieldTypeAvro .. '*'
229+
end
230+
end
231+
if type(fieldTypeAvro) == "table" then
232+
if utils.is_array(fieldTypeAvro) then
233+
-- Union case.
234+
if not utils.value_in("null", fieldTypeAvro) then
235+
table.insert(fieldTypeAvro, "null")
236+
end
237+
else
238+
-- Record case.
239+
check(fieldTypeAvro.type, "fieldTypeAvro.type", "string")
240+
if string.sub(fieldTypeAvro.type, -1) ~= '*' then
241+
fieldTypeAvro.type = fieldTypeAvro.type .. '*'
242+
end
243+
end
244+
end
245+
end
246+
100247
return {
101248
name = convert_schema_helpers.base_name(fieldName),
102249
type = fieldTypeAvro,

0 commit comments

Comments
 (0)