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

Commit ab7ca92

Browse files
committed
Support avro-schema references for records
Fixes #116.
1 parent 7c13a3a commit ab7ca92

9 files changed

+374
-54
lines changed

graphql/tarantool_graphql.lua

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -56,36 +56,61 @@ local default_instance
5656
-- forward declarations
5757
local gql_type
5858

59-
local function avro_type(avro_schema)
59+
local function is_scalar_type(avro_schema_type)
60+
check(avro_schema_type, 'avro_schema_type', 'string')
61+
62+
local scalar_types = {
63+
['int'] = true,
64+
['int*'] = true,
65+
['long'] = true,
66+
['long*'] = true,
67+
--[[
68+
['float'] = true,
69+
['float*'] = true,
70+
['double'] = true,
71+
['double*'] = true,
72+
['boolean'] = true,
73+
['boolean*'] = true,
74+
]]--
75+
['string'] = true,
76+
['string*'] = true,
77+
}
78+
79+
return scalar_types[avro_schema_type] or false
80+
end
81+
82+
local function is_compound_type(avro_schema_type)
83+
check(avro_schema_type, 'avro_schema_type', 'string')
84+
85+
local compound_types = {
86+
['record'] = true,
87+
['record*'] = true,
88+
['array'] = true,
89+
['array*'] = true,
90+
['map'] = true,
91+
['map*'] = true,
92+
}
93+
94+
return compound_types[avro_schema_type] or false
95+
end
96+
97+
local function avro_type(avro_schema, opts)
98+
local opts = opts or {}
99+
local allow_references = opts.allow_references or false
100+
60101
if type(avro_schema) == 'table' then
61-
if avro_schema.type == 'record' then
62-
return 'record'
63-
elseif avro_schema.type == 'record*' then
64-
return 'record*'
102+
if is_compound_type(avro_schema.type) then
103+
return avro_schema.type
65104
elseif utils.is_array(avro_schema) then
66105
return 'union'
67-
elseif avro_schema.type == 'array' then
68-
return 'array'
69-
elseif avro_schema.type == 'array*' then
70-
return 'array*'
71-
elseif avro_schema.type == 'map' then
72-
return 'map'
73-
elseif avro_schema.type == 'map*' then
74-
return 'map*'
106+
elseif allow_references then
107+
return avro_schema
75108
end
76109
elseif type(avro_schema) == 'string' then
77-
if avro_schema == 'int' then
78-
return 'int'
79-
elseif avro_schema == 'int*' then
80-
return 'int*'
81-
elseif avro_schema == 'long' then
82-
return 'long'
83-
elseif avro_schema == 'long*' then
84-
return 'long*'
85-
elseif avro_schema == 'string' then
86-
return 'string'
87-
elseif avro_schema == 'string*' then
88-
return 'string*'
110+
if is_scalar_type(avro_schema) then
111+
return avro_schema
112+
elseif allow_references then
113+
return avro_schema
89114
end
90115
end
91116
error('unrecognized avro-schema type: ' .. json.encode(avro_schema))
@@ -245,14 +270,13 @@ local function convert_record_fields_to_args(fields, opts)
245270
assert(type(field.name) == 'string',
246271
('field.name must be a string, got %s (schema %s)')
247272
:format(type(field.name), json.encode(field)))
248-
249-
-- records, arrays (gql lists) and maps can't be arguments, so these
250-
-- graphql types are to be skipped
251-
local avro_t = avro_type(field.type)
252-
if not skip_compound or (
253-
avro_t ~= 'record' and avro_t ~= 'record*' and
254-
avro_t ~= 'array' and avro_t ~= 'array*' and
255-
avro_t ~= 'map' and avro_t ~= 'map*') then
273+
-- records, arrays (gql lists), maps and unions can't be arguments, so
274+
-- these graphql types are to be skipped;
275+
-- skip_compound == false is the trick for accessor_general-provided
276+
-- record; we don't expect map, array or union here as well as we don't
277+
-- expect avro-schema reference.
278+
local avro_t = avro_type(field.type, {allow_references = true})
279+
if not skip_compound or is_scalar_type(avro_t) then
256280
local gql_class = gql_argument_type(field.type)
257281
args[field.name] = nullable(gql_class)
258282
end
@@ -711,7 +735,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
711735
'state.accessor.list_args must not be nil')
712736

713737
-- type of the top element in the avro-schema
714-
local avro_t = avro_type(avro_schema)
738+
local avro_t = avro_type(avro_schema, {allow_references = true})
715739

716740
if avro_t == 'record' or avro_t == 'record*' then
717741
assert(type(avro_schema.name) == 'string',
@@ -735,6 +759,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
735759
avro_schema.name,
736760
fields = fields,
737761
})
762+
state.declarations[avro_schema.name] = types.nonNull(res)
763+
state.declarations[avro_schema.name .. '*'] = res
738764
return avro_t == 'record' and types.nonNull(res) or res
739765
elseif avro_t == 'enum' then
740766
error('enums not implemented yet') -- XXX
@@ -764,6 +790,12 @@ gql_type = function(state, avro_schema, collection, collection_name)
764790
local gql_map = types_map
765791
return avro_t == 'map' and types.nonNull(gql_map) or gql_map
766792
else
793+
if type(avro_schema) == 'string' then
794+
if state.declarations[avro_schema] ~= nil then
795+
return state.declarations[avro_schema]
796+
end
797+
end
798+
767799
local res = convert_scalar_type(avro_schema, {raise = false})
768800
if res == nil then
769801
error('unrecognized avro-schema type: ' ..
@@ -903,6 +935,9 @@ local function parse_cfg(cfg)
903935
state.list_arguments = utils.gen_booking_table({})
904936
state.all_arguments = utils.gen_booking_table({})
905937

938+
-- map from avro-schema names to graphql types
939+
state.declarations = {}
940+
906941
local accessor = cfg.accessor
907942
assert(accessor ~= nil, 'cfg.accessor must not be nil')
908943
assert(accessor.select ~= nil, 'cfg.accessor.select must not be nil')

test/common/avro_refs.test.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env tarantool
2+
3+
local fio = require('fio')
4+
5+
-- require in-repo version of graphql/ sources despite current working directory
6+
package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)")
7+
:gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' .. package.path
8+
9+
local utils = require('test.utils')
10+
local testdata = require('test.common.lua.test_data_avro_refs')
11+
12+
box.cfg({})
13+
14+
utils.run_testdata(testdata)
15+
16+
os.exit()

test/common/lua/multirunner.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,11 @@ end
165165
local function run(test_run, init_function, cleanup_function, workload)
166166
-- ensure stable order
167167
local names = {}
168-
for conf_name, _ in pairs(CONFS) do
169-
names[#names + 1] = conf_name
168+
for conf_name, conf in pairs(CONFS) do
169+
-- allow to run w/o test-run
170+
if test_run ~= nil or conf.type == 'space' then
171+
names[#names + 1] = conf_name
172+
end
170173
end
171174
table.sort(names)
172175

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
-- Avro-schema references
2+
-- https://github.com/tarantool/graphql/issues/116
3+
4+
-- XXX: check 'fixed' type when we'll support it
5+
-- XXX: check 'enum' type when we'll support it
6+
7+
local tap = require('tap')
8+
local json = require('json')
9+
local yaml = require('yaml')
10+
local utils = require('graphql.utils')
11+
12+
local testdata = {}
13+
14+
testdata.meta = {
15+
schemas = json.decode([[{
16+
"foo": {
17+
"type": "record",
18+
"name": "foo",
19+
"fields": [
20+
{"name": "id", "type": "long"},
21+
{
22+
"name": "bar",
23+
"type": {
24+
"type": "record",
25+
"name": "bar",
26+
"fields": [
27+
{"name": "x", "type": "long"},
28+
{"name": "y", "type": "long"}
29+
]
30+
}
31+
},
32+
{
33+
"name": "bar_ref",
34+
"type": "bar"
35+
},
36+
{
37+
"name": "bar_nref",
38+
"type": "bar*"
39+
},
40+
{
41+
"name": "baz",
42+
"type": {
43+
"type": "record*",
44+
"name": "baz",
45+
"fields": [
46+
{"name": "x", "type": "long"},
47+
{"name": "y", "type": "long"}
48+
]
49+
}
50+
},
51+
{
52+
"name": "baz_ref",
53+
"type": "baz"
54+
},
55+
{
56+
"name": "baz_nref",
57+
"type": "baz*"
58+
}
59+
]
60+
}
61+
}]]),
62+
collections = json.decode([[{
63+
"foo": {
64+
"schema_name": "foo",
65+
"connections": []
66+
}
67+
}]]),
68+
service_fields = {
69+
foo = {},
70+
},
71+
indexes = {
72+
foo = {
73+
id = {
74+
service_fields = {},
75+
fields = {'id'},
76+
index_type = 'tree',
77+
unique = true,
78+
primary = true,
79+
},
80+
},
81+
}
82+
}
83+
84+
function testdata.init_spaces()
85+
-- foo fields
86+
local ID_FN = 1
87+
88+
box.schema.create_space('foo')
89+
box.space.foo:create_index('id', {
90+
type = 'tree', unique = true, parts = {ID_FN, 'unsigned'}})
91+
end
92+
93+
function testdata.drop_spaces()
94+
box.space.foo:drop()
95+
end
96+
97+
function testdata.fill_test_data(virtbox)
98+
local NULL_T = 0
99+
local VALUE_T = 1
100+
101+
local x = 1000
102+
local y = 2000
103+
local a = 3000
104+
local b = 4000
105+
106+
-- non-null bar, baz and its refs
107+
virtbox.foo:replace({1, -- id
108+
x, y, x, y, VALUE_T, {x, y}, -- bar & refs
109+
VALUE_T, {a, b}, a, b, VALUE_T, {a, b}, -- baz & refs
110+
})
111+
-- null in nullable bar, baz refs
112+
virtbox.foo:replace({2, -- id
113+
x, y, x, y, NULL_T, box.NULL, -- bar & refs
114+
NULL_T, box.NULL, a, b, NULL_T, box.NULL, -- baz & refs
115+
})
116+
end
117+
118+
function testdata.run_queries(gql_wrapper)
119+
local test = tap.test('avro_refs')
120+
test:plan(2)
121+
122+
local query_1 = [[
123+
query get_by_id($id: Long) {
124+
foo(id: $id) {
125+
id
126+
bar {
127+
x
128+
y
129+
}
130+
bar_ref {
131+
x
132+
y
133+
}
134+
bar_nref {
135+
x
136+
y
137+
}
138+
baz {
139+
x
140+
y
141+
}
142+
baz_ref {
143+
x
144+
y
145+
}
146+
baz_nref {
147+
x
148+
y
149+
}
150+
}
151+
}
152+
]]
153+
154+
local gql_query_1 = utils.show_trace(function()
155+
return gql_wrapper:compile(query_1)
156+
end)
157+
158+
local variables_1_1 = {id = 1}
159+
local result_1_1 = utils.show_trace(function()
160+
return gql_query_1:execute(variables_1_1)
161+
end)
162+
163+
local exp_result_1_1 = yaml.decode(([[
164+
---
165+
foo:
166+
- id: 1
167+
bar:
168+
x: 1000
169+
y: 2000
170+
bar_ref:
171+
x: 1000
172+
y: 2000
173+
bar_nref:
174+
x: 1000
175+
y: 2000
176+
baz:
177+
x: 3000
178+
y: 4000
179+
baz_ref:
180+
x: 3000
181+
y: 4000
182+
baz_nref:
183+
x: 3000
184+
y: 4000
185+
]]):strip())
186+
187+
test:is_deeply(result_1_1, exp_result_1_1, '1_1')
188+
189+
local variables_1_2 = {id = 2}
190+
local result_1_2 = utils.show_trace(function()
191+
return gql_query_1:execute(variables_1_2)
192+
end)
193+
194+
local exp_result_1_2 = yaml.decode(([[
195+
---
196+
foo:
197+
- id: 2
198+
bar:
199+
x: 1000
200+
y: 2000
201+
bar_ref:
202+
x: 1000
203+
y: 2000
204+
baz_ref:
205+
x: 3000
206+
y: 4000
207+
]]):strip())
208+
209+
test:is_deeply(result_1_2, exp_result_1_2, '1_2')
210+
211+
assert(test:check(), 'check plan')
212+
end
213+
214+
return testdata

0 commit comments

Comments
 (0)