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

Commit 6716b0c

Browse files
committed
Add creation of avro-schema from query with Unions and multihead connections
Close #197, close #84
1 parent cb1a5e2 commit 6716b0c

File tree

3 files changed

+481
-2
lines changed

3 files changed

+481
-2
lines changed

graphql/query_to_avro.lua

+93-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +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
1921
local map_to_avro
22+
local union_to_avro
2023

2124
local gql_scalar_to_avro_index = {
2225
String = "string",
@@ -74,8 +77,10 @@ local function gql_type_to_avro(fieldType, subSelections, context)
7477
result = gql_scalar_to_avro(fieldType)
7578
elseif fieldTypeName == 'Object' then
7679
result = object_to_avro(fieldType, subSelections, context)
77-
elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then
78-
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')
7984
else
8085
error(string.format('Unknown type "%s"', tostring(fieldTypeName)))
8186
end
@@ -97,6 +102,92 @@ map_to_avro = function(mapType)
97102
}
98103
end
99104

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+
100191
--- The function converts a single Object field to avro format.
101192
local function field_to_avro(object_type, fields, context)
102193
local firstField = fields[1]

test/extra/to_avro_multihead.test.lua

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env tarantool
2+
local fio = require('fio')
3+
local yaml = require('yaml')
4+
local avro = require('avro_schema')
5+
local test = require('tap').test('to avro schema')
6+
local testdata = require('test.testdata.multihead_conn_with_nulls_testdata')
7+
local graphql = require('graphql')
8+
9+
-- require in-repo version of graphql/ sources despite current working directory
10+
package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)")
11+
:gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' .. package.path
12+
13+
test:plan(7)
14+
15+
box.cfg{}
16+
17+
testdata.init_spaces()
18+
local meta = testdata.get_test_metadata()
19+
testdata.fill_test_data(box.space, meta)
20+
21+
local gql_wrapper = graphql.new({
22+
schemas = meta.schemas,
23+
collections = meta.collections,
24+
service_fields = meta.service_fields,
25+
indexes = meta.indexes,
26+
accessor = 'space'
27+
})
28+
29+
local query = [[
30+
query obtainHeroes($hero_id: String) {
31+
hero_collection(hero_id: $hero_id) {
32+
hero_id
33+
hero_type
34+
banking_type
35+
hero_connection {
36+
... on box_human_collection {
37+
human_collection {
38+
name
39+
}
40+
}
41+
... on box_starship_collection {
42+
starship_collection {
43+
model
44+
}
45+
}
46+
}
47+
hero_banking_connection {
48+
... on box_array_credit_account_collection {
49+
credit_account_collection {
50+
account_id
51+
hero_banking_id
52+
}
53+
}
54+
... on box_array_dublon_account_collection {
55+
dublon_account_collection {
56+
account_id
57+
hero_banking_id
58+
}
59+
}
60+
}
61+
}
62+
}
63+
]]
64+
65+
local expected_avro_schema = [[
66+
type: record
67+
name: Query
68+
fields:
69+
- name: hero_collection
70+
type:
71+
type: array
72+
items:
73+
type: record
74+
fields:
75+
- name: hero_id
76+
type: string
77+
- name: hero_type
78+
type: string*
79+
- name: banking_type
80+
type: string*
81+
- name: hero_connection
82+
type:
83+
type: record*
84+
name: hero_connection
85+
fields:
86+
- name: human_collection
87+
type:
88+
type: record*
89+
fields:
90+
- name: name
91+
type: string
92+
name: human_collection
93+
namespace: Query.hero_collection
94+
- name: starship_collection
95+
type:
96+
type: record*
97+
fields:
98+
- name: model
99+
type: string
100+
name: starship_collection
101+
namespace: Query.hero_collection
102+
- name: hero_banking_connection
103+
type:
104+
type: record*
105+
name: hero_banking_connection
106+
fields:
107+
- name: credit_account_collection
108+
type:
109+
type: array*
110+
items:
111+
type: record
112+
fields:
113+
- name: account_id
114+
type: string
115+
- name: hero_banking_id
116+
type: string
117+
name: credit_account_collection
118+
namespace: Query.hero_collection
119+
- name: dublon_account_collection
120+
type:
121+
type: array*
122+
items:
123+
type: record
124+
fields:
125+
- name: account_id
126+
type: string
127+
- name: hero_banking_id
128+
type: string
129+
name: dublon_account_collection
130+
namespace: Query.hero_collection
131+
name: hero_collection
132+
namespace: Query
133+
]]
134+
135+
local gql_query = gql_wrapper:compile(query)
136+
local avro_from_query = gql_query:avro_schema()
137+
138+
expected_avro_schema = yaml.decode(expected_avro_schema)
139+
140+
test:is_deeply(avro_from_query, expected_avro_schema,
141+
'comparision between expected and generated (from query) avro-schemas')
142+
143+
local ok, compiled_schema = avro.create(avro_from_query)
144+
assert(ok, tostring(compiled_schema))
145+
146+
local variables_1 = {
147+
hero_id = 'hero_id_1'
148+
}
149+
150+
local result_1 = gql_query:execute(variables_1)
151+
result_1 = result_1.data
152+
local result_expected_1 = yaml.decode([[
153+
hero_collection:
154+
- hero_id: hero_id_1
155+
hero_type: human
156+
hero_connection:
157+
human_collection:
158+
name: Luke
159+
banking_type: credit
160+
hero_banking_connection:
161+
credit_account_collection:
162+
- account_id: credit_account_id_1
163+
hero_banking_id: hero_banking_id_1
164+
- account_id: credit_account_id_2
165+
hero_banking_id: hero_banking_id_1
166+
- account_id: credit_account_id_3
167+
hero_banking_id: hero_banking_id_1
168+
]])
169+
170+
test:is_deeply(result_1, result_expected_1,
171+
'comparision between expected and actual query response 1')
172+
173+
local ok, err = avro.validate(compiled_schema, result_1)
174+
assert(ok, tostring(err))
175+
test:is(ok, true, 'query response validation by avro 1')
176+
177+
local variables_2 = {
178+
hero_id = 'hero_id_2'
179+
}
180+
181+
local result_2 = gql_query:execute(variables_2)
182+
result_2 = result_2.data
183+
local result_expected_2 = yaml.decode([[
184+
hero_collection:
185+
- hero_id: hero_id_2
186+
hero_type: starship
187+
hero_connection:
188+
starship_collection:
189+
model: Falcon-42
190+
banking_type: dublon
191+
hero_banking_connection:
192+
dublon_account_collection:
193+
- account_id: dublon_account_id_1
194+
hero_banking_id: hero_banking_id_2
195+
- account_id: dublon_account_id_2
196+
hero_banking_id: hero_banking_id_2
197+
- account_id: dublon_account_id_3
198+
hero_banking_id: hero_banking_id_2
199+
]])
200+
201+
test:is_deeply(result_2, result_expected_2,
202+
'comparision between expected and actual query response 2')
203+
204+
local ok, err = avro.validate(compiled_schema, result_2)
205+
assert(ok, tostring(err))
206+
test:is(ok, true, 'query response validation by avro 2')
207+
208+
local variables_3 = {
209+
hero_id = 'hero_id_3'
210+
}
211+
212+
local result_3 = gql_query:execute(variables_3)
213+
result_3 = result_3.data
214+
215+
local result_expected_3 = yaml.decode([[
216+
hero_collection:
217+
- hero_id: hero_id_3
218+
]])
219+
220+
test:is_deeply(result_3, result_expected_3,
221+
'comparision between expected and actual query response 3')
222+
223+
local ok, err = avro.validate(compiled_schema, result_3)
224+
assert(ok, tostring(err))
225+
test:is(ok, true, 'query response validation by avro 3')
226+
227+
testdata.drop_spaces()
228+
229+
assert(test:check(), 'check plan')
230+
231+
os.exit()

0 commit comments

Comments
 (0)