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

Commit 22f8b76

Browse files
committed
Filtering over 1:1 connection borders
Fixes #39.
1 parent bfe65bd commit 22f8b76

7 files changed

+385
-17
lines changed

graphql/accessor_general.lua

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,8 @@ end
798798
--- * `pivot_filter` (table, set of fields to match the objected pointed by
799799
--- `offset` arqument of the GraphQL query),
800800
--- * `resulting_object_cnt_max` (number),
801-
--- * `fetched_object_cnt_max` (number).
801+
--- * `fetched_object_cnt_max` (number),
802+
--- * `resolveField` (function) for subrequests, see @{tarantool_graphql.new}.
802803
---
803804
--- @return nil
804805
---
@@ -822,6 +823,7 @@ local function process_tuple(state, tuple, opts)
822823
'query execution timeout exceeded, use `timeout_ms` to increase it')
823824
local collection_name = opts.collection_name
824825
local pcre = opts.pcre
826+
local resolveField = opts.resolveField
825827

826828
-- convert tuple -> object
827829
local obj = opts.unflatten_tuple(collection_name, tuple,
@@ -835,6 +837,20 @@ local function process_tuple(state, tuple, opts)
835837
return true -- skip pivot item too
836838
end
837839

840+
-- make subrequests if needed
841+
for k, v in pairs(filter) do
842+
if obj[k] == nil then
843+
local field_name = k
844+
local sub_filter = v
845+
local sub_opts = {dont_force_nullability = true}
846+
local field = resolveField(field_name, obj, sub_filter, sub_opts)
847+
if field == nil then return true end
848+
obj[k] = field
849+
-- XXX: Remove the value from a filter? But then we need to copy
850+
-- the filter each time in the case.
851+
end
852+
end
853+
838854
-- filter out non-matching objects
839855
local match = utils.is_subtable(obj, filter) and
840856
match_using_re(obj, pcre)
@@ -961,6 +977,7 @@ local function select_internal(self, collection_name, from, filter, args, extra)
961977
unflatten_tuple = self.funcs.unflatten_tuple,
962978
default_unflatten_tuple = default_unflatten_tuple,
963979
pcre = args.pcre,
980+
resolveField = extra.resolveField,
964981
}
965982

966983
if index == nil then

graphql/core/rules.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,11 @@ function rules.uniqueInputObjectFields(node, context)
357357
end
358358
end
359359

360-
validateValue(node.value)
360+
if node.kind == 'inputObject' then
361+
validateValue(node)
362+
else
363+
validateValue(node.value)
364+
end
361365
end
362366

363367
function rules.directivesAreDefined(node, context)

graphql/core/validate.lua

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,19 @@ local visitors = {
268268
rules = { rules.uniqueInputObjectFields }
269269
},
270270

271+
inputObject = {
272+
children = function(node)
273+
return util.map(node.values or {}, function(value)
274+
return value.value
275+
end)
276+
end,
277+
278+
rules = { rules.uniqueInputObjectFields }
279+
},
280+
271281
variable = {
272282
enter = function(node, context)
273-
context.variableReferences[node.name.value] = true
283+
context.variableReferences[node.name.value] = true
274284
end
275285
},
276286

graphql/tarantool_graphql.lua

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ local function separate_args_instance(args_instance, connection_args,
359359
else
360360
error(('cannot found "%s" field ("%s" value) ' ..
361361
'within allowed fields'):format(tostring(k),
362-
tostring(v)))
362+
json.encode(v)))
363363
end
364364
end
365365
return object_args_instance, list_args_instance
@@ -388,23 +388,44 @@ local function convert_simple_connection(state, connection, collection_name)
388388
-- gql type of connection field
389389
local destination_type =
390390
state.nullable_collection_types[c.destination_collection]
391-
392391
assert(destination_type ~= nil,
393392
('destination_type (named %s) must not be nil'):format(
394393
c.destination_collection))
395-
394+
local raw_destination_type = destination_type
396395

397396
local c_args = args_from_destination_collection(state,
398-
c.destination_collection, c.type)
397+
c.destination_collection, c.type)
399398
destination_type = specify_destination_type(destination_type, c.type)
400399

401400
local c_list_args = state.list_arguments[c.destination_collection]
402401

402+
-- capture `raw_destination_type`
403+
local function genResolveField(info)
404+
return function(field_name, object, filter, opts)
405+
assert(raw_destination_type.fields[field_name],
406+
('performing a subrequest by the non-existent ' ..
407+
'field "%s" of the collection "%s"'):format(field_name,
408+
c.destination_collection))
409+
return raw_destination_type.fields[field_name].resolve(
410+
object, filter, info, opts)
411+
end
412+
end
413+
403414
local field = {
404415
name = c.name,
405416
kind = destination_type,
406417
arguments = c_args,
407-
resolve = function(parent, args_instance, info)
418+
resolve = function(parent, args_instance, info, opts)
419+
local opts = opts or {}
420+
assert(type(opts) == 'table',
421+
'opts must be nil or a table, got ' .. type(opts))
422+
local dont_force_nullability =
423+
opts.dont_force_nullability or false
424+
assert(type(dont_force_nullability) == 'boolean',
425+
'opts.dont_force_nullability ' ..
426+
'must be nil or a boolean, got ' ..
427+
type(dont_force_nullability))
428+
408429
local destination_args_names, destination_args_values =
409430
parent_args_values(parent, c.parts)
410431

@@ -432,8 +453,10 @@ local function convert_simple_connection(state, connection, collection_name)
432453
destination_args_names = destination_args_names,
433454
destination_args_values = destination_args_values,
434455
}
456+
local resolveField = genResolveField(info)
435457
local extra = {
436-
qcontext = info.qcontext
458+
qcontext = info.qcontext,
459+
resolveField = resolveField, -- for subrequests
437460
}
438461

439462
-- object_args_instance will be passed to 'filter'
@@ -451,7 +474,8 @@ local function convert_simple_connection(state, connection, collection_name)
451474
-- we expect here exactly one object even for 1:1*
452475
-- connections because we processed all-parts-are-null
453476
-- situation above
454-
assert(#objs == 1, 'expect one matching object, got ' ..
477+
assert(#objs == 1 or dont_force_nullability,
478+
'expect one matching object, got ' ..
455479
tostring(#objs))
456480
return objs[1]
457481
else -- c.type == '1:N'
@@ -778,6 +802,84 @@ local function create_root_collection(state)
778802
})
779803
end
780804

805+
--- Execute a function for each 1:1 or 1:1* connection of each collection.
806+
---
807+
--- @tparam table state tarantool_graphql instance
808+
---
809+
--- @tparam function func a function with the following parameters:
810+
---
811+
--- * source collection name (string);
812+
--- * connection (table).
813+
local function for_each_1_1_connection(state, func)
814+
for collection_name, collection in pairs(state.collections) do
815+
for _, c in ipairs(collection.connections or {}) do
816+
if c.type == '1:1' or c.type == '1:1*' then
817+
func(collection_name, c)
818+
end
819+
end
820+
end
821+
end
822+
823+
--- Add arguments corresponding to 1:1 and 1:1* connections (nested filters).
824+
---
825+
--- @tparam table state graphql_tarantool instance
826+
local function add_connection_arguments(state)
827+
-- map destination collection to list of input objects
828+
local input_objects = {}
829+
-- map source collection and connection name to an input object
830+
local lookup_input_objects = {}
831+
832+
-- create InputObjects for each 1:1 or 1:1* connection of each collection
833+
for_each_1_1_connection(state, function(collection_name, c)
834+
-- XXX: support union collections
835+
if c.variants ~= nil then return end
836+
837+
local object = types.inputObject({
838+
name = c.name,
839+
description = ('generated from the connection "%s" ' ..
840+
'of collection "%s" using collection "%s"'):format(
841+
c.name, collection_name, c.destination_collection),
842+
fields = state.object_arguments[c.destination_collection],
843+
})
844+
845+
if input_objects[c.destination_collection] == nil then
846+
input_objects[c.destination_collection] = {}
847+
end
848+
table.insert(input_objects[c.destination_collection], object)
849+
850+
if lookup_input_objects[collection_name] == nil then
851+
lookup_input_objects[collection_name] = {}
852+
end
853+
lookup_input_objects[collection_name][c.name] = object
854+
end)
855+
856+
-- update fields of collection arguments and input objects with other input
857+
-- objects
858+
for_each_1_1_connection(state, function(collection_name, c)
859+
-- XXX: support union collections
860+
if c.variants ~= nil then return end
861+
862+
local new_object = lookup_input_objects[collection_name][c.name]
863+
-- collection arguments
864+
local fields = state.object_arguments[collection_name]
865+
assert(fields[c.name] == nil,
866+
'we must not add an input object twice to the same collection ' ..
867+
'arguments list')
868+
fields[c.name] = new_object
869+
-- input objects
870+
for _, input_object in ipairs(input_objects[collection_name] or {}) do
871+
local fields = input_object.fields
872+
assert(fields[c.name] == nil,
873+
'we must not add an input object twice to the same input ' ..
874+
'object')
875+
fields[c.name] = {
876+
name = c.name,
877+
kind = new_object,
878+
}
879+
end
880+
end)
881+
end
882+
781883
local function parse_cfg(cfg)
782884
local state = {}
783885

@@ -839,14 +941,25 @@ local function parse_cfg(cfg)
839941
{skip_compound = true})
840942
local list_args = convert_record_fields_to_args(
841943
accessor:list_args(collection_name))
842-
local args = utils.merge_tables(object_args, list_args)
843944

844945
state.object_arguments[collection_name] = object_args
845946
state.list_arguments[collection_name] = list_args
947+
end
948+
949+
add_connection_arguments(state)
950+
951+
-- fill all_arguments with object_arguments + list_arguments
952+
for collection_name, collection in pairs(state.collections) do
953+
local object_args = state.object_arguments[collection_name]
954+
local list_args = state.list_arguments[collection_name]
955+
956+
local args = utils.merge_tables(object_args, list_args)
846957
state.all_arguments[collection_name] = args
847958
end
959+
848960
-- create fake root `Query` collection
849961
create_root_collection(state)
962+
850963
return state
851964
end
852965

@@ -967,10 +1080,14 @@ end
9671080
--- -- destination_args_values = <...>,
9681081
--- -- }
9691082
--- --
970-
--- -- extra is a table which contains additional data for the
971-
--- -- query; by now it consists of a single qcontext table,
972-
--- -- which can be used by accessor to store any query-related
973-
--- -- data
1083+
--- -- `extra` is a table which contains additional data for
1084+
--- -- the query:
1085+
--- --
1086+
--- -- * `qcontext` (table) can be used by an accessor to store
1087+
--- -- any query-related data;
1088+
--- -- * `resolveField(field_name, object, filter, opts)`
1089+
--- -- (function) for performing a subrequest on a fields
1090+
--- -- connected using a 1:1 or 1:1* connection.
9741091
--- --
9751092
--- return ...
9761093
--- end,

test/local/space_nested_args.result

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
2+
3+
+---------------------+
4+
| a-+ h x y |
5+
| |\ \ |\ |
6+
| b c d k l |
7+
| | |\ \ |
8+
| e f g m |
9+
+---------------------+
10+
RESULT
11+
---
12+
order_collection:
13+
- order_id: order_id_1
14+
description: first order of Ivan
15+
user_connection:
16+
user_id: user_id_1
17+
last_name: Ivanov
18+
first_name: Ivan
19+
- order_id: order_id_2
20+
description: second order of Ivan
21+
user_connection:
22+
user_id: user_id_1
23+
last_name: Ivanov
24+
first_name: Ivan
25+
...
26+
27+
RUN upside {{{
28+
QUERY
29+
query emails_trace_upside($upside_body: String) {
30+
email(in_reply_to: {in_reply_to: {body: $upside_body}}) {
31+
body
32+
in_reply_to {
33+
body
34+
in_reply_to {
35+
body
36+
}
37+
}
38+
}
39+
}
40+
VARIABLES
41+
---
42+
upside_body: a
43+
...
44+
45+
RESULT
46+
---
47+
email:
48+
- body: g
49+
in_reply_to:
50+
body: d
51+
in_reply_to:
52+
body: a
53+
- body: f
54+
in_reply_to:
55+
body: d
56+
in_reply_to:
57+
body: a
58+
- body: e
59+
in_reply_to:
60+
body: b
61+
in_reply_to:
62+
body: a
63+
...
64+
65+
}}}
66+

0 commit comments

Comments
 (0)