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

Commit aa89196

Browse files
committed
wip
1 parent 30c64ab commit aa89196

File tree

6 files changed

+437
-7
lines changed

6 files changed

+437
-7
lines changed

graphql/core/schema.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ function schema:generateTypeMap(node)
5050
node.fields = type(node.fields) == 'function' and node.fields() or node.fields
5151
self.typeMap[node.name] = node
5252

53+
if node.__type == 'Union' then
54+
for _, type in ipairs(node.types) do
55+
self:generateTypeMap(type)
56+
end
57+
end
58+
5359
if node.__type == 'Object' and node.interfaces then
5460
for _, interface in ipairs(node.interfaces) do
5561
self:generateTypeMap(interface)

graphql/tarantool_graphql.lua

Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ local function avro_type(avro_schema)
6464
return 'record*'
6565
elseif utils.is_array(avro_schema) then
6666
return 'union'
67+
elseif utils.is_array(avro_schema.type) then
68+
return 'union'
6769
elseif avro_schema.type == 'array' then
6870
return 'array'
6971
elseif avro_schema.type == 'array*' then
@@ -86,6 +88,8 @@ local function avro_type(avro_schema)
8688
return 'string'
8789
elseif avro_schema == 'string*' then
8890
return 'string*'
91+
elseif avro_schema == 'null' then
92+
return 'null'
8993
end
9094
end
9195
error('unrecognized avro-schema type: ' .. json.encode(avro_schema))
@@ -252,8 +256,10 @@ local function convert_record_fields_to_args(fields, opts)
252256
if not skip_compound or (
253257
avro_t ~= 'record' and avro_t ~= 'record*' and
254258
avro_t ~= 'array' and avro_t ~= 'array*' and
255-
avro_t ~= 'map' and avro_t ~= 'map*') then
256-
local gql_class = gql_argument_type(field.type)
259+
avro_t ~= 'map' and avro_t ~= 'map*' and
260+
avro_t ~= 'union') then
261+
262+
local gql_class = gql_argument_type(field.type, field.name)
257263
args[field.name] = nullable(gql_class)
258264
end
259265
end
@@ -275,10 +281,18 @@ local function convert_record_fields(state, fields)
275281
('field.name must be a string, got %s (schema %s)')
276282
:format(type(field.name), json.encode(field)))
277283

278-
res[field.name] = {
279-
name = field.name,
280-
kind = gql_type(state, field.type),
281-
}
284+
-- union case
285+
if utils.is_array(field.type) then
286+
res[field.name] = {
287+
name = field.name,
288+
kind = gql_type(state, {name = field.name, type = field.type}),
289+
}
290+
else
291+
res[field.name] = {
292+
name = field.name,
293+
kind = gql_type(state, field.type),
294+
}
295+
end
282296
end
283297
return res
284298
end
@@ -670,6 +684,161 @@ local convert_connection_to_field = function(state, connection, collection_name)
670684
end
671685
end
672686

687+
--- The function 'boxes' given GraphQL type into GraphQL Object 'box' type.
688+
--- This serves two needs:
689+
--- 1) GraphQL Union types may contain only GraphQL Objects. GraphQL Scalar
690+
--- types maybe put inside Union only if they are 'boxed' inside an Object.
691+
--- 2) GraphQL Union types must have specific form, described in @{create_gql_union}.
692+
--- 'Boxes' is a part of this specific form.
693+
---
694+
--- @tparam table gql_type GraphQL type to be boxed
695+
--- @param avro_schema table or string ... necessary for right names for boxed
696+
--- types. Right names are such names than ... something about validation
697+
--- @treturn table GraphQL Object. Object name is type of initial GraphQL + '_box'.
698+
--- Resulting Object has only one field. Field type is initial GraphQL type.
699+
--- Field name is `avro_name`. Such name is necessary to make GraphQL response
700+
--- valid
701+
--- Consider the following example:
702+
--- String
703+
--- to
704+
--- String_box {
705+
--- string
706+
--- }
707+
local function box_type(gql_type, avro_name)
708+
check(gql_type, 'gql_type', 'table')
709+
710+
local box_name, box_fields
711+
local gql_true_type = nullable(gql_type)
712+
713+
-- also handle avro-schema 'map' case as it is represented as a scalar in
714+
-- our GraphQL schema
715+
if gql_true_type.__type == 'Scalar' then
716+
box_name = gql_true_type.name .. '_box'
717+
box_fields = {[avro_name] = {name = avro_name, kind = gql_type}}
718+
end
719+
720+
if gql_true_type.__type == 'List' then
721+
box_name = gql_true_type.__type .. '_box'
722+
box_fields = {[avro_name] = {name = avro_name, kind = gql_type}}
723+
end
724+
725+
if gql_true_type.__type == 'Object' then
726+
box_name = gql_true_type.name .. '_box'
727+
box_fields = {[avro_name] = {name = avro_name, kind = gql_type}}
728+
end
729+
730+
return types.object({
731+
name = box_name,
732+
description = 'Box (wrapper) around union field',
733+
fields = box_fields,
734+
})
735+
end
736+
737+
--- The functions creates table of GraphQL types from avro-schema union type.
738+
local function create_union_types (avro_schema, state)
739+
check(avro_schema, 'avro_schema', 'table')
740+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array ' ..
741+
', got ' .. yaml.encode(avro_schema))
742+
743+
local union_types = {}
744+
local field_to_type = {}
745+
local is_nullable = false
746+
747+
for _, type in ipairs(avro_schema) do
748+
if type == 'null' then
749+
is_nullable = true
750+
else
751+
union_types[#union_types + 1] = box_type(gql_type(state, type),
752+
type.name or avro_type(type))
753+
local determinant = type.name or type.type or type
754+
field_to_type[determinant] = union_types[#union_types]
755+
end
756+
end
757+
758+
return union_types, field_to_type, is_nullable
759+
end
760+
761+
--- The function creates GraphQL Union type from given avro-schema union type.
762+
--- GraphQL responses, received from tarantool graphql instance should be
763+
--- successfully validated with initial avro-schema (which was used to configure
764+
--- this tarantool graphql instance), so GraphQL Union types must have specific
765+
--- formt to be avro-valid. See example in @treturn for details.
766+
---
767+
--- @tparam table state
768+
--- @tparam table avro_schema avro-schema union type
769+
--- @tparam string union_name name for resulting GraphQL Union type
770+
--- @treturn table GraphQL Union type. Consider the following example:
771+
--- Avro-schema (inside a record):
772+
--- ...
773+
--- "name": "MyUnion", "type": [
774+
--- "null",
775+
--- "string",
776+
--- { "type": "record", "name": "Foo", "fields":[
777+
--- { "name": "foo1", "type": "string" },
778+
--- { "name": "foo2", "type": "string" }
779+
--- ]}
780+
--- ]
781+
--- ...
782+
--- GraphQL Union type:
783+
--- MyUnion {
784+
--- ... on String_box {
785+
--- string
786+
--- }
787+
---
788+
--- ... on Foo_box {
789+
--- Foo {
790+
--- foo1
791+
--- foo2
792+
--- }
793+
--- }
794+
local function create_gql_union(state, avro_schema, union_name)
795+
check(avro_schema, 'avro_schema', 'table')
796+
assert(utils.is_array(avro_schema), 'union avro-schema must be an array ' ..
797+
', got ' .. yaml.encode(avro_schema))
798+
799+
-- check avro-schema constraints
800+
for i, type in ipairs(avro_schema) do
801+
assert(avro_type(type) ~= 'union', 'unions must not immediately ' ..
802+
'contain other unions')
803+
804+
if type.name == nil then
805+
for j, another_type in ipairs(avro_schema) do
806+
if i ~= j then
807+
assert(avro_type(type) ~= avro_type(another_type),
808+
'Unions may not contain more than one schema with ' ..
809+
'the same type except for the named types ' ..
810+
'record, fixed and enum')
811+
end
812+
end
813+
end
814+
end
815+
816+
-- create GraphQL union
817+
local union_types, field_to_type, is_nullable =
818+
create_union_types(avro_schema, state)
819+
820+
local union_type = types.union({
821+
types = union_types,
822+
name = union_name,
823+
resolveType = function(result)
824+
for determinant, type in pairs(field_to_type) do
825+
if result[determinant] ~= nil then
826+
return type
827+
end
828+
end
829+
error(('result objects "%s" has no determinant field matching ' ..
830+
'determinants for this union "%s"'):format(yaml.encode(result),
831+
yaml.encode(field_to_type)))
832+
end
833+
})
834+
835+
if not is_nullable then
836+
union_type = types.nonNull(union_type)
837+
end
838+
839+
return union_type
840+
end
841+
673842
--- The function converts passed avro-schema to a GraphQL type.
674843
---
675844
--- @tparam table state for read state.accessor and previously filled
@@ -763,6 +932,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
763932

764933
local gql_map = types_map
765934
return avro_t == 'map' and types.nonNull(gql_map) or gql_map
935+
elseif avro_t == 'union' then
936+
return create_gql_union(state, avro_schema.type, avro_schema.name)
766937
else
767938
local res = convert_scalar_type(avro_schema, {raise = false})
768939
if res == nil then

graphql/utils.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,13 @@ function utils.table_size(t)
204204
return count
205205
end
206206

207+
function utils.is_in_array(key, array)
208+
for _, v in ipairs(array) do
209+
if key == v then
210+
return true
211+
end
212+
end
213+
return false
214+
end
215+
207216
return utils

test/local/avro_union.result

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
RESULT
2+
---
3+
user_collection:
4+
- user_id: user_id_0
5+
name: Nobody
6+
- user_id: user_id_1
7+
name: Zlata
8+
stuff:
9+
string: Some string
10+
- user_id: user_id_2
11+
name: Ivan
12+
stuff:
13+
int: 123
14+
- user_id: user_id_3
15+
name: Jane
16+
stuff:
17+
map: {'salary': 333, 'deposit': 444}
18+
- user_id: user_id_4
19+
name: Dan
20+
stuff:
21+
Foo:
22+
foo1: foo1 string
23+
foo2: foo2 string
24+
- user_id: user_id_5
25+
name: Max
26+
stuff:
27+
array:
28+
- {'salary': 'salary string', 'deposit': 'deposit string'}
29+
- {'salary': 'string salary', 'deposit': 'string deposit'}
30+
...
31+
32+
Validating results with initial avro-schema
33+
false
34+
Field stuff missing
35+
---
36+
user_id: user_id_0
37+
name: Nobody
38+
...
39+
40+
true
41+
---
42+
user_id: user_id_1
43+
name: Zlata
44+
stuff:
45+
string: Some string
46+
...
47+
48+
true
49+
---
50+
user_id: user_id_2
51+
name: Ivan
52+
stuff:
53+
int: 123
54+
...
55+
56+
true
57+
---
58+
user_id: user_id_3
59+
name: Jane
60+
stuff:
61+
map: {'salary': 333, 'deposit': 444}
62+
...
63+
64+
true
65+
---
66+
user_id: user_id_4
67+
name: Dan
68+
stuff:
69+
Foo:
70+
foo1: foo1 string
71+
foo2: foo2 string
72+
...
73+
74+
true
75+
---
76+
user_id: user_id_5
77+
name: Max
78+
stuff:
79+
array:
80+
- {'salary': 'salary string', 'deposit': 'deposit string'}
81+
- {'salary': 'string salary', 'deposit': 'string deposit'}
82+
...
83+

0 commit comments

Comments
 (0)