@@ -369,6 +369,51 @@ local function specify_destination_type(destination_type, connection_type)
369
369
end
370
370
end
371
371
372
+ --- The function 'boxes' given collection type.
373
+ ---
374
+ --- Why the 'boxing' of collection types is needed and how it is done is
375
+ --- described in comments to @{convert_multihead_connection}.
376
+ ---
377
+ --- @tparam table type_to_box GraphQL Object type (which represents a collection)
378
+ --- @tparam string connection_type of given collection (1:1, 1:1* or 1:N)
379
+ --- @tparam string type_to_box_name name of given 'type_to_box' (It can not
380
+ --- be taken from 'type_to_box' because at the time of function execution
381
+ --- 'type_to_box' refers to an empty table, which later will be filled with
382
+ --- actual type table)
383
+ --- @treturn table GraphQL Object type representing 'boxed' collection
384
+ --- @treturn string name of the single field in the box GraphQL Object
385
+ local function box_collection_type (type_to_box , connection_type , type_to_box_name )
386
+ check (type_to_box , ' type_to_box' , ' table' )
387
+ check (connection_type , ' connection_type' , ' string' )
388
+ check (type_to_box_name , ' type_to_box_name' , ' string' )
389
+
390
+ local box_type_name
391
+ local box_type_description
392
+
393
+ if connection_type == ' 1:1' then
394
+ box_type_name = ' box_' .. type_to_box_name
395
+ box_type_description = ' Box around 1:1 multi-head variant'
396
+ elseif connection_type == ' 1:1*' then
397
+ box_type_name = ' box_' .. type_to_box_name
398
+ box_type_description = ' Box around 1:1* multi-head variant'
399
+ elseif connection_type == ' 1:N' then
400
+ box_type_name = ' box_array_' .. type_to_box_name
401
+ box_type_description = ' Box around 1:N multi-head variant'
402
+ else
403
+ error (' unknown connection type: ' .. tostring (connection_type ))
404
+ end
405
+
406
+ local field_name = type_to_box_name
407
+ local box_field = {[field_name ] = {name = field_name , kind = type_to_box }}
408
+ local box_type = types .object ({
409
+ name = box_type_name ,
410
+ description = box_type_description ,
411
+ fields = box_field
412
+ })
413
+
414
+ return box_type , field_name
415
+ end
416
+
372
417
local function parent_args_values (parent , connection_parts )
373
418
local destination_args_names = {}
374
419
local destination_args_values = {}
@@ -449,26 +494,20 @@ end
449
494
--- connection
450
495
local function convert_simple_connection (state , connection , collection_name )
451
496
local c = connection
452
- assert (type (c .destination_collection ) == ' string' ,
453
- ' connection.destination_collection must be a string, got ' ..
454
- type (c .destination_collection ))
455
- assert (type (c .parts ) == ' table' ,
456
- ' connection.parts must be a table, got ' .. type (c .parts ))
497
+
498
+ check (c .destination_collection , ' connection.destination_collection' , ' string' )
499
+ check (c .parts , ' connection.parts' , ' table' )
457
500
458
501
-- gql type of connection field
459
502
local destination_type =
460
503
state .nullable_collection_types [c .destination_collection ]
461
504
assert (destination_type ~= nil ,
462
505
(' destination_type (named %s) must not be nil' ):format (
463
506
c .destination_collection ))
464
- local raw_destination_type = destination_type
465
507
466
- local c_args = args_from_destination_collection (state ,
467
- c .destination_collection , c .type )
508
+ local raw_destination_type = destination_type
468
509
destination_type = specify_destination_type (destination_type , c .type )
469
510
470
- local c_list_args = state .list_arguments [c .destination_collection ]
471
-
472
511
-- capture `raw_destination_type`
473
512
local function genResolveField (info )
474
513
return function (field_name , object , filter , opts )
@@ -481,6 +520,10 @@ local function convert_simple_connection(state, connection, collection_name)
481
520
end
482
521
end
483
522
523
+ local c_args = args_from_destination_collection (state ,
524
+ c .destination_collection , c .type )
525
+ local c_list_args = state .list_arguments [c .destination_collection ]
526
+
484
527
local field = {
485
528
name = c .name ,
486
529
kind = destination_type ,
@@ -559,45 +602,113 @@ local function convert_simple_connection(state, connection, collection_name)
559
602
return field
560
603
end
561
604
562
- --- The function converts passed union connection to a field of GraphQL type.
563
- --- It combines destination collections of passed union connection into
564
- --- the Union GraphQL type.
565
- --- (destination collections are 'types' of a 'Union' in GraphQL).
605
+ --- The function converts passed multi-head connection to GraphQL Union type.
606
+ ---
607
+ --- Destination collections of passed multi-head connection are turned into
608
+ --- variants of resulting GraphQL Union type. Note that GraphQL types which
609
+ --- represent destination collections are wrapped with 'box' types. Here is 'how'
610
+ --- and 'why' it is done.
611
+ ---
612
+ --- How:
613
+ --- Let's consider multi-head connection with two destination collections:
614
+ --- "human": {
615
+ --- "name": "human",
616
+ --- "type": "record",
617
+ --- "fields": [
618
+ --- { "name": "hero_id", "type": "string" },
619
+ --- { "name": "name", "type": "string" }
620
+ --- ]
621
+ --- }
622
+ ---
623
+ --- "starship": {
624
+ --- "name": "starship",
625
+ --- "type": "record",
626
+ --- "fields": [
627
+ --- { "name": "hero_id", "type": "string" },
628
+ --- { "name": "model", "type": "string" }
629
+ --- ]
630
+ --- }
631
+ ---
632
+ --- In case of 1:1 multi-head connection the resulting field can be accessed as
633
+ --- follows:
634
+ --- hero_connection {
635
+ --- ... on box_human_collection {
636
+ --- human_collection {
637
+ --- name
638
+ --- }
639
+ --- }
640
+ --- ... on box_starship_collection {
641
+ --- starship_collection {
642
+ --- model
643
+ --- }
644
+ --- }
645
+ --- }
646
+ ---
647
+ --- In case of 1:N multi-head connection:
648
+ --- hero_connection {
649
+ --- ... on box_array_human_collection {
650
+ --- human_collection {
651
+ --- name
652
+ --- }
653
+ --- }
654
+ --- ... on box_array_starship_collection {
655
+ --- starship_collection {
656
+ --- model
657
+ --- }
658
+ --- }
659
+ --- }
660
+ ---
661
+ --- Why:
662
+ --- There are two reasons for 'boxing'.
663
+ --- 1) In case of 1:N connections, destination collections are represented by
664
+ --- GraphQL Lists (of Objects). But according to the GraphQL specification only
665
+ --- Objects can be variants of Union. So we need to 'box' Lists (into Objects
666
+ --- with single field) to use them as Union variants.
667
+ --- 2) GraphQL responses, received from tarantool graphql, must be avro-valid.
668
+ --- On every incoming GraphQL query a corresponding avro-schema can be generated.
669
+ --- Response to this query is 'avro-valid' if it can be successfully validated with
670
+ --- this generated (from incoming query) avro-schema. In case of multi-head
671
+ --- connections it means that value of multi-head connection field must have
672
+ --- the following format: SomeDestinationCollectionType: {...} where {...}
673
+ --- indicates the YAML encoding of a SomeDestinationCollectionType instance.
674
+ --- In case of 1:N {...} indicates a list of instances. Using of 'boxing'
675
+ --- provides the needed format.
566
676
---
567
677
--- @tparam table state for collection types
568
- --- @tparam table connection union connection to create field on
678
+ --- @tparam table connection multi-head connection to create GraphQL Union on
569
679
--- @tparam table collection_name name of the collection which has given
570
680
--- connection
571
- local function convert_union_connection (state , connection , collection_name )
681
+ --- @treturn table GraphQL Union type
682
+ local function convert_multihead_connection (state , connection , collection_name )
572
683
local c = connection
573
684
local union_types = {}
574
685
local collection_to_arguments = {}
575
686
local collection_to_list_arguments = {}
687
+ local var_num_to_box_field_name = {}
576
688
577
689
for _ , v in ipairs (c .variants ) do
578
690
assert (v .determinant , ' each variant should have a determinant' )
579
- assert (type (v .determinant ) == ' table' , ' variant\' s determinant ' ..
580
- ' must end be a table, got ' .. type (v .determinant ))
581
- assert (type (v .destination_collection ) == ' string' ,
582
- ' variant.destination_collection must be a string, got ' ..
583
- type (v .destination_collection ))
584
- assert (type (v .parts ) == ' table' ,
585
- ' variant.parts must be a table, got ' .. type (v .parts ))
691
+ check (v .determinant , ' variant\' s determinant' , ' table' )
692
+ check (v .destination_collection , ' variant.destination_collection' , ' string' )
693
+ check (v .parts , ' variant.parts' , ' table' )
586
694
587
695
local destination_type =
588
696
state .nullable_collection_types [v .destination_collection ]
589
697
assert (destination_type ~= nil ,
590
698
(' destination_type (named %s) must not be nil' ):format (
591
699
v .destination_collection ))
700
+ destination_type = specify_destination_type (destination_type , c .type )
701
+
702
+ local variant_type , box_field_name = box_collection_type (destination_type ,
703
+ c .type , v .destination_collection )
704
+ var_num_to_box_field_name [# union_types + 1 ] = box_field_name
705
+ union_types [# union_types + 1 ] = variant_type
592
706
593
707
local v_args = args_from_destination_collection (state ,
594
708
v .destination_collection , c .type )
595
- destination_type = specify_destination_type (destination_type , c .type )
596
709
597
710
local v_list_args = state .list_arguments [v .destination_collection ]
598
711
599
- union_types [# union_types + 1 ] = destination_type
600
-
601
712
collection_to_arguments [v .destination_collection ] = v_args
602
713
collection_to_list_arguments [v .destination_collection ] = v_list_args
603
714
end
@@ -612,21 +723,22 @@ local function convert_union_connection(state, connection, collection_name)
612
723
' Determinant keys:\n "%s"' ):
613
724
format (yaml .encode (parent ), yaml .encode (determinant_keys )))
614
725
615
- local variant_num
616
- local resulting_variant
617
- for i , variant in ipairs (c .variants ) do
618
- variant_num = i
619
- local is_match = utils .is_subtable (parent , variant .determinant )
620
-
726
+ local var_idx
727
+ local res_var
728
+ for i , var in ipairs (c .variants ) do
729
+ local is_match = utils .is_subtable (parent , var .determinant )
621
730
if is_match then
622
- resulting_variant = variant
731
+ res_var = var
732
+ var_idx = i
623
733
break
624
734
end
625
735
end
626
736
627
- assert (resulting_variant , (' Variant resolving failed.' ..
737
+ local box_field_name = var_num_to_box_field_name [var_idx ]
738
+
739
+ assert (res_var , (' Variant resolving failed.' ..
628
740
' Parent object: "%s"\n ' ):format (yaml .encode (parent )))
629
- return resulting_variant , variant_num
741
+ return res_var , var_idx , box_field_name
630
742
end
631
743
632
744
local field = {
@@ -637,8 +749,9 @@ local function convert_union_connection(state, connection, collection_name)
637
749
}),
638
750
arguments = nil , -- see Border cases/Unions at the top of the file
639
751
resolve = function (parent , args_instance , info )
640
- local v , variant_num = resolve_variant (parent )
752
+ local v , variant_num , box_field_name = resolve_variant (parent )
641
753
local destination_type = union_types [variant_num ]
754
+
642
755
local destination_collection =
643
756
state .nullable_collection_types [v .destination_collection ]
644
757
local destination_args_names , destination_args_values =
@@ -693,9 +806,16 @@ local function convert_union_connection(state, connection, collection_name)
693
806
-- situation above
694
807
assert (# objs == 1 , ' expect one matching object, got ' ..
695
808
tostring (# objs ))
696
- return objs [1 ], destination_type
809
+
810
+ -- this 'wrapping' is needed because we use 'select' on
811
+ -- 'collection' GraphQL type and the result of the resolve function
812
+ -- must be in {'collection_name': {result}} format to
813
+ -- be avro-valid
814
+ local formatted_obj = {[box_field_name ] = objs [1 ]}
815
+ return formatted_obj , destination_type
697
816
else -- c.type == '1:N'
698
- return objs , destination_type
817
+ local formatted_objs = {[box_field_name ] = objs }
818
+ return formatted_objs , destination_type
699
819
end
700
820
end
701
821
}
@@ -720,33 +840,33 @@ local convert_connection_to_field = function(state, connection, collection_name)
720
840
assert (type (connection .name ) == ' string' ,
721
841
' connection.name must be a string, got ' .. type (connection .name ))
722
842
assert (connection .destination_collection or connection .variants ,
723
- ' connection must either destination_collection or variatns field' )
843
+ ' connection must either destination_collection or variants field' )
724
844
725
845
if connection .destination_collection then
726
846
return convert_simple_connection (state , connection , collection_name )
727
847
end
728
848
729
849
if connection .variants then
730
- return convert_union_connection (state , connection , collection_name )
850
+ return convert_multihead_connection (state , connection , collection_name )
731
851
end
732
852
end
733
853
734
854
--- The function 'boxes' given GraphQL type into GraphQL Object 'box' type.
735
855
---
736
- --- @tparam table gql_type GraphQL type to be boxed
737
- --- @tparam string avro_name type (or name, in record case) of avro-schema which
738
- --- was used to create `gql_type`. `avro_name` is used to provide avro-valid names
739
- --- for fields of boxed types
856
+ --- @tparam table type_to_box GraphQL type to be boxed
857
+ --- @tparam string box_field_name name of the single box field
740
858
--- @treturn table GraphQL Object
741
- local function box_type (gql_type , avro_name )
742
- check (gql_type , ' gql_type' , ' table' )
859
+ local function box_type (type_to_box , box_field_name )
860
+ check (type_to_box , ' type_to_box' , ' table' )
861
+ check (box_field_name , ' box_field_name' , ' string' )
743
862
744
- local gql_true_type = nullable (gql_type )
863
+ local gql_true_type = nullable (type_to_box )
745
864
746
865
local box_name = gql_true_type .name or gql_true_type .__type
747
866
box_name = box_name .. ' _box'
748
867
749
- local box_fields = {[avro_name ] = {name = avro_name , kind = gql_type }}
868
+ local box_fields = {[box_field_name ] = {name = box_field_name ,
869
+ kind = type_to_box }}
750
870
751
871
return types .object ({
752
872
name = box_name ,
0 commit comments