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

Commit d9d74a4

Browse files
committed
Filtering over 1:1 connection borders
Fixes #39.
1 parent 73e12cb commit d9d74a4

7 files changed

+377
-14
lines changed

graphql/accessor_general.lua

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,8 @@ end
755755
--- * `pivot_filter` (table, set of fields to match the objected pointed by
756756
--- `offset` arqument of the GraphQL query),
757757
--- * `resulting_object_cnt_max` (number),
758-
--- * `fetched_object_cnt_max` (number).
758+
--- * `fetched_object_cnt_max` (number),
759+
--- * `resolveField` (function) for subrequests, see @{tarantool_graphql.new}.
759760
---
760761
--- @return nil
761762
---
@@ -779,6 +780,7 @@ local function process_tuple(state, tuple, opts)
779780
'query execution timeout exceeded, use `timeout_ms` to increase it')
780781
local collection_name = opts.collection_name
781782
local pcre = opts.pcre
783+
local resolveField = opts.resolveField
782784

783785
-- convert tuple -> object
784786
local obj = opts.unflatten_tuple(collection_name, tuple,
@@ -792,6 +794,20 @@ local function process_tuple(state, tuple, opts)
792794
return true -- skip pivot item too
793795
end
794796

797+
-- make subrequests if needed
798+
for k, v in pairs(filter) do
799+
if obj[k] == nil then
800+
local field_name = k
801+
local sub_filter = v
802+
local sub_opts = {dont_force_nullability = true}
803+
local field = resolveField(field_name, obj, sub_filter, sub_opts)
804+
if field == nil then return true end
805+
obj[k] = field
806+
-- XXX: Remove the value from a filter? But then we need to copy
807+
-- the filter each time in the case.
808+
end
809+
end
810+
795811
-- filter out non-matching objects
796812
local match = utils.is_subtable(obj, filter) and
797813
match_using_re(obj, pcre)
@@ -918,6 +934,7 @@ local function select_internal(self, collection_name, from, filter, args, extra)
918934
unflatten_tuple = self.funcs.unflatten_tuple,
919935
default_unflatten_tuple = default_unflatten_tuple,
920936
pcre = args.pcre,
937+
resolveField = extra.resolveField,
921938
}
922939

923940
if index == nil then

graphql/core/rules.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,11 @@ function rules.uniqueInputObjectFields(node, context)
349349
end
350350
end
351351

352-
validateValue(node.value)
352+
if node.kind == 'inputObject' then
353+
validateValue(node)
354+
else
355+
validateValue(node.value)
356+
end
353357
end
354358

355359
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: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
323323
assert(destination_type ~= nil,
324324
('destination_type (named %s) must not be nil'):format(
325325
c.destination_collection))
326+
local raw_destination_type = destination_type
326327

327328
local c_args
328329
if c.type == '1:1' then
@@ -340,11 +341,33 @@ gql_type = function(state, avro_schema, collection, collection_name)
340341

341342
local c_list_args = state.list_arguments[c.destination_collection]
342343

344+
-- capture `raw_destination_type`
345+
local function genResolveField(info)
346+
return function(field_name, object, filter, opts)
347+
assert(raw_destination_type.fields[field_name],
348+
('performing a subrequest by the non-existent ' ..
349+
'field "%s" of the collection "%s"'):format(field_name,
350+
c.destination_collection))
351+
return raw_destination_type.fields[field_name].resolve(
352+
object, filter, info, opts)
353+
end
354+
end
355+
343356
fields[c.name] = {
344357
name = c.name,
345358
kind = destination_type,
346359
arguments = c_args,
347-
resolve = function(parent, args_instance, info)
360+
resolve = function(parent, args_instance, info, opts)
361+
local opts = opts or {}
362+
assert(type(opts) == 'table',
363+
'opts must be nil or a table, got ' .. type(opts))
364+
local dont_force_nullability =
365+
opts.dont_force_nullability or false
366+
assert(type(dont_force_nullability) == 'boolean',
367+
'opts.dont_force_nullability ' ..
368+
'must be nil or a boolean, got ' ..
369+
type(dont_force_nullability))
370+
348371
local destination_args_names = {}
349372
local destination_args_values = {}
350373
local are_all_parts_non_null = true
@@ -407,8 +430,10 @@ gql_type = function(state, avro_schema, collection, collection_name)
407430
destination_args_names = destination_args_names,
408431
destination_args_values = destination_args_values,
409432
}
433+
local resolveField = genResolveField(info)
410434
local extra = {
411-
qcontext = info.qcontext
435+
qcontext = info.qcontext,
436+
resolveField = resolveField, -- for subrequests
412437
}
413438
local object_args_instance = {} -- passed to 'filter'
414439
local list_args_instance = {} -- passed to 'args'
@@ -420,7 +445,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
420445
else
421446
error(('cannot found "%s" field ("%s" value) ' ..
422447
'within allowed fields'):format(tostring(k),
423-
tostring(v)))
448+
json.encode(v)))
424449
end
425450
end
426451
local objs = accessor:select(parent,
@@ -433,7 +458,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
433458
-- we expect here exactly one object even for 1:1*
434459
-- connections because we processed all-parts-are-null
435460
-- situation above
436-
assert(#objs == 1,
461+
assert(#objs == 1 or dont_force_nullability,
437462
'expect one matching object, got ' ..
438463
tostring(#objs))
439464
return objs[1]
@@ -529,6 +554,78 @@ local function create_root_collection(state)
529554
})
530555
end
531556

557+
--- Execute a function for each 1:1 or 1:1* connection of each collection.
558+
---
559+
--- @tparam table state tarantool_graphql instance
560+
---
561+
--- @tparam function func a function with the following parameters:
562+
---
563+
--- * source collection name (string);
564+
--- * connection (table).
565+
local function for_each_1_1_connection(state, func)
566+
for collection_name, collection in pairs(state.collections) do
567+
for _, c in ipairs(collection.connections or {}) do
568+
if c.type == '1:1' or c.type == '1:1*' then
569+
func(collection_name, c)
570+
end
571+
end
572+
end
573+
end
574+
575+
--- Add arguments corresponding to 1:1 and 1:1* connections (nested filters).
576+
---
577+
--- @tparam table state graphql_tarantool instance
578+
local function add_connection_arguments(state)
579+
-- map destination collection to list of input objects
580+
local input_objects = {}
581+
-- map source collection and connection name to an input object
582+
local lookup_input_objects = {}
583+
584+
-- create InputObjects for each 1:1 or 1:1* connection of each collection
585+
for_each_1_1_connection(state, function(collection_name, c)
586+
local object = types.inputObject({
587+
name = c.name,
588+
description = ('generated from the connection "%s" ' ..
589+
'of collection "%s" using collection "%s"'):format(
590+
c.name, collection_name, c.destination_collection),
591+
fields = state.object_arguments[c.destination_collection],
592+
})
593+
594+
if input_objects[c.destination_collection] == nil then
595+
input_objects[c.destination_collection] = {}
596+
end
597+
table.insert(input_objects[c.destination_collection], object)
598+
599+
if lookup_input_objects[collection_name] == nil then
600+
lookup_input_objects[collection_name] = {}
601+
end
602+
lookup_input_objects[collection_name][c.name] = object
603+
end)
604+
605+
-- update fields of collection arguments and input objects with other input
606+
-- objects
607+
for_each_1_1_connection(state, function(collection_name, c)
608+
local new_object = lookup_input_objects[collection_name][c.name]
609+
-- collection arguments
610+
local fields = state.object_arguments[collection_name]
611+
assert(fields[c.name] == nil,
612+
'we must not add an input object twice to the same collection ' ..
613+
'arguments list')
614+
fields[c.name] = new_object
615+
-- input objects
616+
for _, input_object in ipairs(input_objects[collection_name] or {}) do
617+
local fields = input_object.fields
618+
assert(fields[c.name] == nil,
619+
'we must not add an input object twice to the same input ' ..
620+
'object')
621+
fields[c.name] = {
622+
name = c.name,
623+
kind = new_object,
624+
}
625+
end
626+
end)
627+
end
628+
532629
local function parse_cfg(cfg)
533630
local state = {}
534631

@@ -590,14 +687,25 @@ local function parse_cfg(cfg)
590687
{skip_compound = true})
591688
local list_args = convert_record_fields_to_args(
592689
accessor:list_args(collection_name))
593-
local args = utils.merge_tables(object_args, list_args)
594690

595691
state.object_arguments[collection_name] = object_args
596692
state.list_arguments[collection_name] = list_args
693+
end
694+
695+
add_connection_arguments(state)
696+
697+
-- fill all_arguments with object_arguments + list_arguments
698+
for collection_name, collection in pairs(state.collections) do
699+
local object_args = state.object_arguments[collection_name]
700+
local list_args = state.list_arguments[collection_name]
701+
702+
local args = utils.merge_tables(object_args, list_args)
597703
state.all_arguments[collection_name] = args
598704
end
705+
599706
-- create fake root `Query` collection
600707
create_root_collection(state)
708+
601709
return state
602710
end
603711

@@ -718,10 +826,14 @@ end
718826
--- -- destination_args_values = <...>,
719827
--- -- }
720828
--- --
721-
--- -- extra is a table which contains additional data for the
722-
--- -- query; by now it consists of a single qcontext table,
723-
--- -- which can be used by accessor to store any query-related
724-
--- -- data
829+
--- -- `extra` is a table which contains additional data for
830+
--- -- the query:
831+
--- --
832+
--- -- * `qcontext` (table) can be used by an accessor to store
833+
--- -- any query-related data;
834+
--- -- * `resolveField(field_name, object, filter, opts)`
835+
--- -- (function) for performing a subrequest on a fields
836+
--- -- connected using a 1:1 or 1:1* connection.
725837
--- --
726838
--- return ...
727839
--- 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)