@@ -10,12 +10,16 @@ local introspection = require(path .. '.introspection')
10
10
local query_util = require (path .. ' .query_util' )
11
11
local avro_helpers = require (' graphql.avro_helpers' )
12
12
local convert_schema_helpers = require (' graphql.convert_schema.helpers' )
13
+ local utils = require (' graphql.utils' )
14
+ local check = utils .check
13
15
14
16
-- module functions
15
17
local query_to_avro = {}
16
18
17
19
-- forward declaration
18
20
local object_to_avro
21
+ local union_to_avro
22
+ local map_to_avro
19
23
20
24
local gql_scalar_to_avro_index = {
21
25
String = " string" ,
@@ -29,7 +33,9 @@ local gql_scalar_to_avro_index = {
29
33
30
34
local function gql_scalar_to_avro (fieldType )
31
35
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
33
39
local result = gql_scalar_to_avro_index [fieldType .name ]
34
40
assert (result ~= nil , " Unexpected scalar type: " .. fieldType .name )
35
41
return result
45
51
--- @tparam table context current traversal context, here it just falls to the
46
52
--- called functions (internal graphql-lua format)
47
53
---
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
+ ---
48
60
--- @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 )
50
62
local fieldTypeName = fieldType .__type
51
63
local isNonNull = false
52
64
@@ -71,20 +83,115 @@ local function gql_type_to_avro(fieldType, subSelections, context)
71
83
result = gql_scalar_to_avro (fieldType )
72
84
elseif fieldTypeName == ' Object' then
73
85
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' )
76
90
else
77
91
error (string.format (' Unknown type "%s"' , tostring (fieldTypeName )))
78
92
end
79
93
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
81
97
result = avro_helpers .make_avro_type_nullable (result , {
82
98
raise_on_nullable = true ,
83
99
})
84
100
end
85
101
return result
86
102
end
87
103
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
+
88
195
--- The function converts a single Object field to avro format.
89
196
local function field_to_avro (object_type , fields , context )
90
197
local firstField = fields [1 ]
@@ -97,6 +204,46 @@ local function field_to_avro(object_type, fields, context)
97
204
98
205
local fieldTypeAvro = gql_type_to_avro (fieldType .kind , subSelections ,
99
206
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
+
100
247
return {
101
248
name = convert_schema_helpers .base_name (fieldName ),
102
249
type = fieldTypeAvro ,
0 commit comments