230
230
--- Convert each field of an avro-schema to a graphql type.
231
231
---
232
232
--- @tparam table state for read state.accessor and previously filled
233
- --- state.types
233
+ --- state.nullable_collection_types
234
234
--- @tparam table fields fields part from an avro-schema
235
235
---
236
236
--- @treturn table `res` -- map with type names as keys and graphql types as
253
253
--- The function converts passed avro-schema to a GraphQL type.
254
254
---
255
255
--- @tparam table state for read state.accessor and previously filled
256
- --- state.types (state.types are gql types)
256
+ --- state.nullable_collection_types (those are gql types)
257
257
--- @tparam table avro_schema input avro-schema
258
258
--- @tparam [opt] table collection table with schema_name, connections fields
259
259
--- described a collection (e.g. tarantool's spaces)
@@ -307,8 +307,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
307
307
for _ , c in ipairs ((collection or {}).connections or {}) do
308
308
assert (type (c .type ) == ' string' ,
309
309
' connection.type must be a string, got ' .. type (c .type ))
310
- assert (c .type == ' 1:1' or c .type == ' 1:N' ,
311
- ' connection.type must be 1:1 or 1:N, got ' .. c .type )
310
+ assert (c .type == ' 1:1' or c .type == ' 1:1* ' or c . type == ' 1: N' ,
311
+ ' connection.type must be 1:1, 1:1* or 1:N, got ' .. c .type )
312
312
assert (type (c .name ) == ' string' ,
313
313
' connection.name must be a string, got ' .. type (c .name ))
314
314
assert (type (c .destination_collection ) == ' string' ,
@@ -319,16 +319,20 @@ gql_type = function(state, avro_schema, collection, collection_name)
319
319
320
320
-- gql type of connection field
321
321
local destination_type =
322
- state .types [c .destination_collection ]
322
+ state .nullable_collection_types [c .destination_collection ]
323
323
assert (destination_type ~= nil ,
324
324
(' destination_type (named %s) must not be nil' ):format (
325
325
c .destination_collection ))
326
326
327
327
local c_args
328
328
if c .type == ' 1:1' then
329
+ destination_type = types .nonNull (destination_type )
330
+ c_args = state .object_arguments [c .destination_collection ]
331
+ elseif c .type == ' 1:1*' then
329
332
c_args = state .object_arguments [c .destination_collection ]
330
333
elseif c .type == ' 1:N' then
331
- destination_type = types .nonNull (types .list (destination_type ))
334
+ destination_type = types .nonNull (types .list (types .nonNull (
335
+ destination_type )))
332
336
c_args = state .all_arguments [c .destination_collection ]
333
337
else
334
338
error (' unknown connection type: ' .. tostring (c .type ))
@@ -343,6 +347,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
343
347
resolve = function (parent , args_instance , info )
344
348
local destination_args_names = {}
345
349
local destination_args_values = {}
350
+ local are_all_parts_non_null = true
351
+ local are_all_parts_null = true
346
352
347
353
for _ , part in ipairs (c .parts ) do
348
354
assert (type (part .source_field ) == ' string' ,
@@ -354,8 +360,43 @@ gql_type = function(state, avro_schema, collection, collection_name)
354
360
355
361
destination_args_names [# destination_args_names + 1 ] =
356
362
part .destination_field
363
+
364
+ local value = parent [part .source_field ]
357
365
destination_args_values [# destination_args_values + 1 ] =
358
- parent [part .source_field ]
366
+ value
367
+
368
+ if value ~= nil then -- nil or box.NULL
369
+ are_all_parts_null = false
370
+ else
371
+ are_all_parts_non_null = false
372
+ end
373
+ end
374
+
375
+ -- check FULL match constraint before request of
376
+ -- destination object(s)
377
+ --
378
+ -- note that connection key parts can be prefix of index
379
+ -- key parts
380
+ --
381
+ -- zero parts count considered as ok by this check
382
+ local is_full_match_ok = are_all_parts_null or
383
+ are_all_parts_non_null
384
+ if not is_full_match_ok then -- avoid extra json.encode()
385
+ assert (is_full_match_ok ,
386
+ ' FULL MATCH constraint was failed: connection ' ..
387
+ ' key parts must be all non-nulls or all nulls; ' ..
388
+ ' object: ' .. json .encode (parent ))
389
+ end
390
+
391
+ -- When all parts are null:
392
+ -- * return null for 1:1* connection;
393
+ -- * avoid non-needed index lookup for 1:N connection and
394
+ -- return the empty list.
395
+ if are_all_parts_null then
396
+ assert (c .type == ' 1:1*' or c .type == ' 1:N' ,
397
+ ' only 1:1* or 1:N connections can have ' ..
398
+ ' all key parts null' )
399
+ return c .type == ' 1:N' and {} or nil
359
400
end
360
401
361
402
local from = {
@@ -386,7 +427,10 @@ gql_type = function(state, avro_schema, collection, collection_name)
386
427
assert (type (objs ) == ' table' ,
387
428
' objs list received from an accessor ' ..
388
429
' must be a table, got ' .. type (objs ))
389
- if c .type == ' 1:1' then
430
+ if c .type == ' 1:1' or c .type == ' 1:1*' then
431
+ -- we expect here exactly one object even for 1:1*
432
+ -- connections because we processed all-parts-are-null
433
+ -- situation above
390
434
assert (# objs == 1 ,
391
435
' expect one matching object, got ' ..
392
436
tostring (# objs ))
@@ -405,7 +449,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
405
449
avro_schema .name ,
406
450
fields = fields ,
407
451
})
408
- return avro_t == ' enum ' and types .nonNull (res ) or res
452
+ return avro_t == ' record ' and types .nonNull (res ) or res
409
453
elseif avro_t == ' enum' then
410
454
error (' enums not implemented yet' ) -- XXX
411
455
elseif avro_t == ' array' or avro_t == ' array*' then
445
489
446
490
local function parse_cfg (cfg )
447
491
local state = {}
448
- state .types = utils .gen_booking_table ({})
492
+
493
+ -- collection type is always record, so always non-null; we can lazily
494
+ -- evaluate non-null type from nullable type, but not vice versa, so we
495
+ -- collect nullable types here and evaluate non-null ones where needed
496
+ state .nullable_collection_types = utils .gen_booking_table ({})
497
+
449
498
state .object_arguments = utils .gen_booking_table ({})
450
499
state .list_arguments = utils .gen_booking_table ({})
451
500
state .all_arguments = utils .gen_booking_table ({})
@@ -481,8 +530,15 @@ local function parse_cfg(cfg)
481
530
assert (schema .type == ' record' ,
482
531
' top-level schema must have record avro type, got ' ..
483
532
tostring (schema .type ))
484
- state .types [collection_name ] = gql_type (state , schema , collection ,
485
- collection_name )
533
+ local collection_type =
534
+ gql_type (state , schema , collection , collection_name )
535
+ -- we utilize the fact that collection type is always non-null and
536
+ -- don't store this information; see comment above for
537
+ -- `nullable_collection_types` variable definition
538
+ assert (collection_type .__type == ' NonNull' ,
539
+ ' collection must always has non-null type' )
540
+ state .nullable_collection_types [collection_name ] =
541
+ nullable (collection_type )
486
542
487
543
-- prepare arguments' types
488
544
local object_args = convert_record_fields_to_args (schema .fields ,
@@ -497,7 +553,8 @@ local function parse_cfg(cfg)
497
553
498
554
-- create entry points from collection names
499
555
fields [collection_name ] = {
500
- kind = types .nonNull (types .list (state .types [collection_name ])),
556
+ kind = types .nonNull (types .list (types .nonNull (
557
+ state .nullable_collection_types [collection_name ]))),
501
558
arguments = state .all_arguments [collection_name ],
502
559
resolve = function (rootValue , args_instance , info )
503
560
local object_args_instance = {} -- passed to 'filter'
0 commit comments