diff --git a/.luacheckrc b/.luacheckrc index 562ec37..5179565 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -3,7 +3,7 @@ std = { 'tonumber', 'type', 'assert', 'ipairs', 'math', 'error', 'string', 'table', 'pairs', 'os', 'select', 'unpack', 'dofile', 'next', 'getmetatable', 'setmetatable', 'rawget', 'print', 'shard_status', - 'loadstring' + 'loadstring', }, globals = {'package'} } diff --git a/graphql/tarantool_graphql.lua b/graphql/tarantool_graphql.lua index d597525..d116ee6 100644 --- a/graphql/tarantool_graphql.lua +++ b/graphql/tarantool_graphql.lua @@ -105,9 +105,7 @@ end --- Non-recursive version of the @{gql_type} function that returns --- InputObject instead of Object. -local function gql_argument_type(state, avro_schema) - assert(type(state) == 'table', - 'state must be a table, got ' .. type(state)) +local function gql_argument_type(avro_schema) assert(avro_schema ~= nil, 'avro_schema must not be nil') @@ -149,27 +147,53 @@ local function gql_argument_type(state, avro_schema) end end -local function convert_record_fields_to_args(state, fields) +--- Convert each field of an avro-schema to a scalar graphql type or an input +--- object. +--- +--- @tparam table fields list of fields of the avro-schema record fields format +--- +--- @tparam[opt] table opts optional options: +--- +--- * `skip_compound` -- do not add fields of record type to the arguments; +--- default: false. +--- +--- @treturn table `args` -- map with type names as keys and graphql types as +--- values +local function convert_record_fields_to_args(fields, opts) + assert(type(fields) == 'table', + 'fields must be a table, got ' .. type(fields)) + + local opts = opts or {} + assert(type(opts) == 'table', + 'opts must be a table, got ' .. type(opts)) + + local skip_compound = opts.skip_compound or false + assert(type(skip_compound) == 'boolean', + 'skip_compound must be a boolean, got ' .. type(skip_compound)) + local args = {} for _, field in ipairs(fields) do assert(type(field.name) == 'string', ('field.name must be a string, got %s (schema %s)') :format(type(field.name), json.encode(field))) - local gql_class = gql_argument_type(state, field.type) - args[field.name] = nullable(gql_class) + if not skip_compound or avro_type(field.type) ~= 'record' then + local gql_class = gql_argument_type(field.type) + args[field.name] = nullable(gql_class) + end end return args end ---- Convert each field of an avro-schema to a graphql type and corresponding ---- argument for an upper graphql type. +--- Convert each field of an avro-schema to a graphql type. --- --- @tparam table state for read state.accessor and previously filled --- state.types --- @tparam table fields fields part from an avro-schema +--- +--- @treturn table `res` -- map with type names as keys and graphql types as +--- values local function convert_record_fields(state, fields) local res = {} - local object_args = {} for _, field in ipairs(fields) do assert(type(field.name) == 'string', ('field.name must be a string, got %s (schema %s)') @@ -178,9 +202,8 @@ local function convert_record_fields(state, fields) name = field.name, kind = gql_type(state, field.type), } - object_args[field.name] = nullable(res[field.name].kind) end - return res, object_args + return res end --- The function recursively converts passed avro-schema to a graphql type. @@ -228,7 +251,7 @@ gql_type = function(state, avro_schema, collection, collection_name) ('avro_schema.fields must be a table, got %s (avro_schema %s)') :format(type(avro_schema.fields), json.encode(avro_schema))) - local fields, _ = convert_record_fields(state, avro_schema.fields) + local fields = convert_record_fields(state, avro_schema.fields) for _, c in ipairs((collection or {}).connections or {}) do assert(type(c.type) == 'string', @@ -371,10 +394,10 @@ local function parse_cfg(cfg) schema.name)) state.types[name] = gql_type(state, schema, collection, name) - local _, object_args = convert_record_fields(state, - schema.fields) + local object_args = convert_record_fields_to_args(schema.fields, + {skip_compound = true}) local list_args = convert_record_fields_to_args( - state, accessor:list_args(name)) + accessor:list_args(name)) local args = utils.merge_tables(object_args, list_args) state.object_arguments[name] = object_args state.list_arguments[name] = list_args diff --git a/test/common/lua/multirunner.lua b/test/common/lua/multirunner.lua index 0140180..9e5f97d 100755 --- a/test/common/lua/multirunner.lua +++ b/test/common/lua/multirunner.lua @@ -1,6 +1,8 @@ #!/usr/bin/env tarantool + local net_box = require('net.box') -local shard = require('shard') + +local initialized = false local function instance_uri(instance_id) local socket_dir = require('fio').cwd() @@ -9,13 +11,21 @@ local function instance_uri(instance_id) end local function init_shard(test_run, servers, config) - local shard = require('shard') - local suite = "common" + local suite = 'common' test_run:create_cluster(servers, suite) - box.once('init_shard_module', function() + + -- XXX: for now we always use one shard configuration (a first one), + -- because it is unclear how to reload shard module with an another + -- configuration; the another way is use several test configurations, but + -- it seems to be non-working in 'core = app' tests with current test-run. + -- Ways to better handle this is subject to future digging. + local shard = require('shard') + if not initialized then shard.init(config) - end) + initialized = true + end shard.wait_connection() + return shard end local function shard_cleanup(test_run, servers) @@ -29,6 +39,14 @@ local function shard_cleanup(test_run, servers) end end +local function for_each_server(shard, func) + for _, zone in ipairs(shard.shards) do + for _, node in ipairs(zone) do + func(node.uri) + end + end +end + -- Run tests on multiple accessors and configurations. -- Feel free to add more configurations. local function run(test_run, init_function, cleanup_function, callback) @@ -38,8 +56,8 @@ local function run(test_run, init_function, cleanup_function, callback) local servers = {'shard1', 'shard2', 'shard3', 'shard4'}; - -- Test sharding without redundancy = 2. - init_shard(test_run, servers, { + -- Test sharding with redundancy = 2. + local shard = init_shard(test_run, servers, { servers = { { uri = instance_uri('1'), zone = '0' }, { uri = instance_uri('2'), zone = '1' }, @@ -50,23 +68,23 @@ local function run(test_run, init_function, cleanup_function, callback) password = '', redundancy = 2, monitor = false - }); + }) - for _, shard_server in ipairs(shard_status().online) do - local c = net_box.connect(shard_server.uri) + for_each_server(shard, function(uri) + local c = net_box.connect(uri) c:eval(init_script) - end + end) callback("Shard 2x2", shard) - for _, shard_server in ipairs(shard_status().online) do - local c = net_box.connect(shard_server.uri) + for_each_server(shard, function(uri) + local c = net_box.connect(uri) c:eval(cleanup_script) - end + end) shard_cleanup(test_run, servers) -- Test sharding without redundancy. - init_shard(test_run, servers, { + local shard = init_shard(test_run, servers, { servers = { { uri = instance_uri('1'), zone = '0' }, { uri = instance_uri('2'), zone = '1' }, @@ -77,19 +95,19 @@ local function run(test_run, init_function, cleanup_function, callback) password = '', redundancy = 1, monitor = false - }); + }) - for _, shard_server in ipairs(shard_status().online) do - local c = net_box.connect(shard_server.uri) + for_each_server(shard, function(uri) + local c = net_box.connect(uri) c:eval(init_script) - end + end) callback("Shard 4x1", shard) - for _, shard_server in ipairs(shard_status().online) do - local c = net_box.connect(shard_server.uri) + for_each_server(shard, function(uri) + local c = net_box.connect(uri) c:eval(cleanup_script) - end + end) shard_cleanup(test_run, servers) -- Test local setup (box). diff --git a/test/common/lua/test_data_nested_record.lua b/test/common/lua/test_data_nested_record.lua new file mode 100644 index 0000000..31c82ed --- /dev/null +++ b/test/common/lua/test_data_nested_record.lua @@ -0,0 +1,141 @@ +-- Nested record inside a record +-- https://github.com/tarantool/graphql/issues/46 +-- https://github.com/tarantool/graphql/issues/49 + +local json = require('json') +local yaml = require('yaml') +local utils = require('graphql.utils') + +local testdata = {} + +testdata.meta = { + schemas = json.decode([[{ + "user": { + "type": "record", + "name": "user", + "fields": [ + {"name": "uid", "type": "long"}, + {"name": "p1", "type": "string"}, + {"name": "p2", "type": "string"}, + { + "name": "nested", + "type": { + "type": "record", + "name": "nested", + "fields": [ + {"name": "x", "type": "long"}, + {"name": "y", "type": "long"} + ] + } + } + ] + } + }]]), + collections = json.decode([[{ + "user": { + "schema_name": "user", + "connections": [] + } + }]]), + service_fields = { + user = {}, + }, + indexes = { + user = { + uid = { + service_fields = {}, + fields = {'uid'}, + index_type = 'tree', + unique = true, + primary = true, + }, + }, + } +} + + +function testdata.init_spaces() + -- user fields + local UID_FN = 1 + + box.schema.create_space('user') + box.space.user:create_index('uid', { + type = 'tree', unique = true, parts = {UID_FN, 'unsigned'}}) +end + +function testdata.drop_spaces() + box.space.user:drop() +end + +function testdata.fill_test_data(virtbox) + for i = 1, 15 do + local uid = i + local p1 = 'p1 ' .. tostring(i) + local p2 = 'p2 ' .. tostring(i) + local x = 1000 + i + local y = 2000 + i + virtbox.user:replace({uid, p1, p2, x, y}) + end +end + +function testdata.run_queries(gql_wrapper) + local output = '' + + local query_1 = [[ + query getUserByUid($uid: Long) { + user(uid: $uid) { + uid + p1 + p2 + nested { + x + y + } + } + } + ]] + + local variables_1 = {uid = 5} + local result_1 = utils.show_trace(function() + local gql_query_1 = gql_wrapper:compile(query_1) + return gql_query_1:execute(variables_1) + end) + + output = output .. 'RUN 1 {{{\n' .. + (('QUERY\n%s'):format(query_1:rstrip())) .. '\n' .. + (('VARIABLES\n%s'):format(yaml.encode(variables_1))) .. '\n' .. + (('RESULT\n%s'):format(yaml.encode(result_1))) .. '\n' .. + '}}}\n' + + --[=[ + local query_2 = [[ + query getUserByX($x: Long) { + user(nested: {x: $x}) { + uid + p1 + p2 + nested { + x + y + } + } + } + ]] + + local variables_2 = {x = 1005} + local result_2 = utils.show_trace(function() + local gql_query_2 = gql_wrapper:compile(query_2) + return gql_query_2:execute(variables_2) + end) + + output = output .. 'RUN 2 {{{\n' .. + (('QUERY\n%s'):format(query_2:rstrip())) .. '\n' .. + (('VARIABLES\n%s'):format(yaml.encode(variables_2))) .. '\n' .. + (('RESULT\n%s'):format(yaml.encode(result_2))) .. '\n' .. + '}}}\n' + ]=]-- + + return output:rstrip() +end + +return testdata diff --git a/test/common/nested_record.result b/test/common/nested_record.result new file mode 100644 index 0000000..bfefbcd --- /dev/null +++ b/test/common/nested_record.result @@ -0,0 +1,93 @@ +Shard 2x2 +RUN 1 {{{ +QUERY + query getUserByUid($uid: Long) { + user(uid: $uid) { + uid + p1 + p2 + nested { + x + y + } + } + } +VARIABLES +--- +uid: 5 +... + +RESULT +--- +user: +- p2: p2 5 + p1: p1 5 + uid: 5 + nested: + y: 2005 + x: 1005 +... + +}}} +Shard 4x1 +RUN 1 {{{ +QUERY + query getUserByUid($uid: Long) { + user(uid: $uid) { + uid + p1 + p2 + nested { + x + y + } + } + } +VARIABLES +--- +uid: 5 +... + +RESULT +--- +user: +- p2: p2 5 + p1: p1 5 + uid: 5 + nested: + y: 2005 + x: 1005 +... + +}}} +Local (box) +RUN 1 {{{ +QUERY + query getUserByUid($uid: Long) { + user(uid: $uid) { + uid + p1 + p2 + nested { + x + y + } + } + } +VARIABLES +--- +uid: 5 +... + +RESULT +--- +user: +- p2: p2 5 + p1: p1 5 + uid: 5 + nested: + y: 2005 + x: 1005 +... + +}}} diff --git a/test/common/nested_record.test.lua b/test/common/nested_record.test.lua new file mode 100755 index 0000000..1fbdfea --- /dev/null +++ b/test/common/nested_record.test.lua @@ -0,0 +1,41 @@ +#!/usr/bin/env tarantool + +local fio = require('fio') +local multirunner = require('multirunner') +local testdata = require('test_data_nested_record') +local test_run = require('test_run').new() +local graphql = require('graphql') + +box.cfg({}) + +-- require in-repo version of graphql/ sources despite current working directory +package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)") + :gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' .. package.path + +local function run(setup_name, shard) + print(setup_name) + + local accessor_class = shard and graphql.accessor_shard or + graphql.accessor_space + local virtbox = shard or box.space + + local accessor = accessor_class.new({ + schemas = testdata.meta.schemas, + collections = testdata.meta.collections, + service_fields = testdata.meta.service_fields, + indexes = testdata.meta.indexes, + }) + + local gql_wrapper = graphql.new({ + schemas = testdata.meta.schemas, + collections = testdata.meta.collections, + accessor = accessor, + }) + + testdata.fill_test_data(virtbox) + print(testdata.run_queries(gql_wrapper)) +end + +multirunner.run(test_run, testdata.init_spaces, testdata.drop_spaces, run) + +os.exit() diff --git a/test/common/suite.ini b/test/common/suite.ini index fc75004..3371ad2 100644 --- a/test/common/suite.ini +++ b/test/common/suite.ini @@ -1,4 +1,5 @@ [default] core = app description = tests different setups simultaneously -lua_libs = lua/multirunner.lua lua/test_data_user_order.lua +lua_libs = lua/multirunner.lua lua/test_data_user_order.lua \ + lua/test_data_nested_record.lua