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

Commit 040b92b

Browse files
authored
Merge pull request #201 from tarantool/union_map_directive_to_avro
Add creation of avro-schema from query with multi-head connections, Unions, Maps and directives
2 parents 20dc371 + 286cb8d commit 040b92b

13 files changed

+1754
-184
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ local compiled_query = graphql_lib.compile(query)
124124
local result = compiled_query:execute(variables)
125125
```
126126

127+
### Multi-head connections
128+
A parent object is matching against a multi-head connection variants in the
129+
order of the variants. The parent object should match with a determinant of
130+
at least one variant except the following case. When source fields of all
131+
variants are null the multi-head connection obligated to give null object as
132+
the result. In this case the parent object is allowed to don’t match any variant.
133+
One can use this feature to avoid to set any specific determinant value when a
134+
multi-head connection is known to have no connected object.
135+
127136
### Mutations
128137

129138
Mutations are disabled for avro-schema-2\*, because it can work incorrectly for
@@ -368,6 +377,10 @@ git clone https://github.com/tarantool/graphql.git
368377
git submodule update --recursive --init
369378
make test
370379
```
380+
To run specific test:
381+
```
382+
TEST_RUN_TESTS=common/mutation make test
383+
```
371384

372385
## Requirements
373386

graphql/convert_schema/resolve.lua

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ end
3333
---
3434
--- Note that connection key parts can be prefix of index key parts. Zero parts
3535
--- count considered as ok by this check.
36-
local function are_all_parts_null(parent, connection_parts)
36+
local function are_all_parts_null(parent, connection_parts, opts)
3737
local are_all_parts_null = true
3838
local are_all_parts_non_null = true
3939
for _, part in ipairs(connection_parts) do
@@ -47,7 +47,9 @@ local function are_all_parts_null(parent, connection_parts)
4747
end
4848

4949
local ok = are_all_parts_null or are_all_parts_non_null
50-
if not ok then -- avoid extra json.encode()
50+
local opts = opts or {}
51+
local no_assert = opts.no_assert or false
52+
if not ok and not no_assert then -- avoid extra json.encode()
5153
assert(ok,
5254
'FULL MATCH constraint was failed: connection ' ..
5355
'key parts must be all non-nulls or all nulls; ' ..
@@ -190,6 +192,22 @@ function resolve.gen_resolve_function_multihead(collection_name, connection,
190192
end
191193

192194
return function(parent, _, info)
195+
-- If a parent object does not have all source fields (for any of
196+
-- variants) non-null then we do not resolve variant and just return
197+
-- box.NULL.
198+
local is_source_fields_found = false
199+
for _, variant in ipairs(c.variants) do
200+
is_source_fields_found =
201+
not are_all_parts_null(parent, variant.parts, {no_assert = true})
202+
if is_source_fields_found then
203+
break
204+
end
205+
end
206+
207+
if not is_source_fields_found then
208+
return box.NULL, nil
209+
end
210+
193211
local v, variant_num, box_field_name = resolve_variant(parent)
194212
local destination_type = union_types[variant_num]
195213

graphql/convert_schema/types.lua

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,12 @@ 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})
453-
454-
local res = core_types.map
451+
table.insert(context.path, 'Map')
452+
local converted_values = types.convert(state, avro_schema.values,
453+
{context = context})
454+
table.remove(context.path, #context.path)
455+
local map_name = helpers.full_name('Map', context)
456+
local res = core_types.map({values = converted_values, name = map_name})
455457
return avro_t == 'map' and core_types.nonNull(res) or res
456458
elseif avro_t == 'union' then
457459
return union.convert(avro_schema, {

graphql/core/types.lua

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,17 +191,26 @@ 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+
instance.nonNull = types.nonNull(instance)
211+
212+
return instance
213+
end
205214

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

graphql/impl.lua

Lines changed: 3 additions & 3 deletions
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

Lines changed: 128 additions & 3 deletions
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 map_to_avro
22+
local union_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
@@ -71,8 +77,10 @@ local function gql_type_to_avro(fieldType, subSelections, context)
7177
result = gql_scalar_to_avro(fieldType)
7278
elseif fieldTypeName == 'Object' then
7379
result = object_to_avro(fieldType, subSelections, context)
74-
elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then
75-
error('Interfaces and Unions are not supported yet')
80+
elseif fieldTypeName == 'Union' then
81+
result = union_to_avro(fieldType, subSelections, context)
82+
elseif fieldTypeName == 'Interface' then
83+
error('Interfaces are not supported yet')
7684
else
7785
error(string.format('Unknown type "%s"', tostring(fieldTypeName)))
7886
end
@@ -85,6 +93,101 @@ local function gql_type_to_avro(fieldType, subSelections, context)
8593
return result
8694
end
8795

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

98201
local fieldTypeAvro = gql_type_to_avro(fieldType.kind, subSelections,
99202
context)
203+
-- Currently we support only 'include' and 'skip' directives. Both of them
204+
-- affect resulting avro-schema the same way: field with directive becomes
205+
-- nullable, if it's already not. Nullable field does not change.
206+
--
207+
-- If it is a 1:N connection then it's 'array' field becomes 'array*'.
208+
-- If it is avro-schema union, then 'null' will be added to the union
209+
-- types. If there are more then one directive on a field then all works
210+
-- the same way, like it is only one directive. (But we still check all
211+
-- directives to be 'include' or 'skip').
212+
if firstField.directives ~= nil then
213+
for _, d in ipairs(firstField.directives) do
214+
check(d.name, "directive.name", "table")
215+
check(d.arguments, "directive.arguments", "table")
216+
check(d.kind, "directive.kind", "string")
217+
assert(d.kind == "directive")
218+
check(d.name.value, "directive.name.value", "string")
219+
assert(d.name.value == "include" or d.name.value == "skip",
220+
"Only 'include' and 'skip' directives are supported for now")
221+
end
222+
fieldTypeAvro = avro_helpers.make_avro_type_nullable(fieldTypeAvro)
223+
end
224+
100225
return {
101226
name = convert_schema_helpers.base_name(fieldName),
102227
type = fieldTypeAvro,

0 commit comments

Comments
 (0)