@@ -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 map_to_avro
22
+ local union_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
@@ -71,8 +77,10 @@ local function gql_type_to_avro(fieldType, subSelections, context)
71
77
result = gql_scalar_to_avro (fieldType )
72
78
elseif fieldTypeName == ' Object' then
73
79
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' )
76
84
else
77
85
error (string.format (' Unknown type "%s"' , tostring (fieldTypeName )))
78
86
end
@@ -85,6 +93,101 @@ local function gql_type_to_avro(fieldType, subSelections, context)
85
93
return result
86
94
end
87
95
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
+
88
191
--- The function converts a single Object field to avro format.
89
192
local function field_to_avro (object_type , fields , context )
90
193
local firstField = fields [1 ]
@@ -97,6 +200,28 @@ local function field_to_avro(object_type, fields, context)
97
200
98
201
local fieldTypeAvro = gql_type_to_avro (fieldType .kind , subSelections ,
99
202
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
+
100
225
return {
101
226
name = convert_schema_helpers .base_name (fieldName ),
102
227
type = fieldTypeAvro ,
0 commit comments