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

Commit a3c4275

Browse files
committed
Add support of 1:N multi-head connections
add multi-head connections GQL Union representation avro-valid; closes #118, 105
1 parent 9bfc529 commit a3c4275

File tree

2 files changed

+361
-57
lines changed

2 files changed

+361
-57
lines changed

graphql/tarantool_graphql.lua

Lines changed: 168 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,51 @@ local function specify_destination_type(destination_type, connection_type)
369369
end
370370
end
371371

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+
372417
local function parent_args_values(parent, connection_parts)
373418
local destination_args_names = {}
374419
local destination_args_values = {}
@@ -449,26 +494,20 @@ end
449494
--- connection
450495
local function convert_simple_connection(state, connection, collection_name)
451496
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')
457500

458501
-- gql type of connection field
459502
local destination_type =
460503
state.nullable_collection_types[c.destination_collection]
461504
assert(destination_type ~= nil,
462505
('destination_type (named %s) must not be nil'):format(
463506
c.destination_collection))
464-
local raw_destination_type = destination_type
465507

466-
local c_args = args_from_destination_collection(state,
467-
c.destination_collection, c.type)
508+
local raw_destination_type = destination_type
468509
destination_type = specify_destination_type(destination_type, c.type)
469510

470-
local c_list_args = state.list_arguments[c.destination_collection]
471-
472511
-- capture `raw_destination_type`
473512
local function genResolveField(info)
474513
return function(field_name, object, filter, opts)
@@ -481,6 +520,10 @@ local function convert_simple_connection(state, connection, collection_name)
481520
end
482521
end
483522

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+
484527
local field = {
485528
name = c.name,
486529
kind = destination_type,
@@ -559,45 +602,113 @@ local function convert_simple_connection(state, connection, collection_name)
559602
return field
560603
end
561604

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.
566676
---
567677
--- @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
569679
--- @tparam table collection_name name of the collection which has given
570680
--- 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)
572683
local c = connection
573684
local union_types = {}
574685
local collection_to_arguments = {}
575686
local collection_to_list_arguments = {}
687+
local var_num_to_box_field_name = {}
576688

577689
for _, v in ipairs(c.variants) do
578690
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')
586694

587695
local destination_type =
588696
state.nullable_collection_types[v.destination_collection]
589697
assert(destination_type ~= nil,
590698
('destination_type (named %s) must not be nil'):format(
591699
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
592706

593707
local v_args = args_from_destination_collection(state,
594708
v.destination_collection, c.type)
595-
destination_type = specify_destination_type(destination_type, c.type)
596709

597710
local v_list_args = state.list_arguments[v.destination_collection]
598711

599-
union_types[#union_types + 1] = destination_type
600-
601712
collection_to_arguments[v.destination_collection] = v_args
602713
collection_to_list_arguments[v.destination_collection] = v_list_args
603714
end
@@ -612,21 +723,22 @@ local function convert_union_connection(state, connection, collection_name)
612723
'Determinant keys:\n"%s"'):
613724
format(yaml.encode(parent), yaml.encode(determinant_keys)))
614725

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)
621730
if is_match then
622-
resulting_variant = variant
731+
res_var = var
732+
var_idx = i
623733
break
624734
end
625735
end
626736

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.'..
628740
'Parent object: "%s"\n'):format(yaml.encode(parent)))
629-
return resulting_variant, variant_num
741+
return res_var, var_idx, box_field_name
630742
end
631743

632744
local field = {
@@ -637,8 +749,9 @@ local function convert_union_connection(state, connection, collection_name)
637749
}),
638750
arguments = nil, -- see Border cases/Unions at the top of the file
639751
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)
641753
local destination_type = union_types[variant_num]
754+
642755
local destination_collection =
643756
state.nullable_collection_types[v.destination_collection]
644757
local destination_args_names, destination_args_values =
@@ -693,9 +806,16 @@ local function convert_union_connection(state, connection, collection_name)
693806
-- situation above
694807
assert(#objs == 1, 'expect one matching object, got ' ..
695808
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
697816
else -- c.type == '1:N'
698-
return objs, destination_type
817+
local formatted_objs = {[box_field_name] = objs}
818+
return formatted_objs, destination_type
699819
end
700820
end
701821
}
@@ -720,33 +840,33 @@ local convert_connection_to_field = function(state, connection, collection_name)
720840
assert(type(connection.name) == 'string',
721841
'connection.name must be a string, got ' .. type(connection.name))
722842
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')
724844

725845
if connection.destination_collection then
726846
return convert_simple_connection(state, connection, collection_name)
727847
end
728848

729849
if connection.variants then
730-
return convert_union_connection(state, connection, collection_name)
850+
return convert_multihead_connection(state, connection, collection_name)
731851
end
732852
end
733853

734854
--- The function 'boxes' given GraphQL type into GraphQL Object 'box' type.
735855
---
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
740858
--- @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')
743862

744-
local gql_true_type = nullable(gql_type)
863+
local gql_true_type = nullable(type_to_box)
745864

746865
local box_name = gql_true_type.name or gql_true_type.__type
747866
box_name = box_name .. '_box'
748867

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 }}
750870

751871
return types.object({
752872
name = box_name,

0 commit comments

Comments
 (0)