diff --git a/graphql/query_to_avro.lua b/graphql/query_to_avro.lua index 3843a55..ff68c30 100644 --- a/graphql/query_to_avro.lua +++ b/graphql/query_to_avro.lua @@ -135,18 +135,22 @@ object_to_avro = function(object_type, selections, context) return result end ---- Create an Avro schema for a given query. +--- Create an Avro schema for a given query / operation. --- ---- @tparam table query object which avro schema should be created for +--- @tparam table qstate compiled query for which the avro schema should be +--- created --- ---- @treturn table `avro_schema` avro schema for any `query:execute()` result -function query_to_avro.convert(query) - assert(type(query) == "table", - ('query should be a table, got: %s; ' .. +--- @tparam[opt] string operation_name optional operation name +--- +--- @treturn table `avro_schema` avro schema for any +--- `qstate:execute(..., operation_name)` result +function query_to_avro.convert(qstate, operation_name) + assert(type(qstate) == "table", + ('qstate should be a table, got: %s; ' .. 'hint: use ":" instead of "."'):format(type(table))) - local state = query.state - local context = query_util.buildContext(state.schema, query.ast, {}, {}, - query.operation_name) + local state = qstate.state + local context = query_util.buildContext(state.schema, qstate.ast, {}, {}, + operation_name) -- The variable is necessary to avoid fullname interferention. -- Each nested Avro record creates it's namespace. context.namespace_parts = {} diff --git a/graphql/server/server.lua b/graphql/server/server.lua index 4c7429f..ae58b25 100644 --- a/graphql/server/server.lua +++ b/graphql/server/server.lua @@ -107,7 +107,14 @@ function server.init(graphql, host, port) } end - local ok, result = pcall(compiled_query.execute, compiled_query, variables) + local operation_name = parsed.operationName + -- box.NULL -> nil + if operation_name == nil then + operation_name = nil + end + + local ok, result = pcall(compiled_query.execute, compiled_query, + variables, operation_name) if not ok then return { status = 200, diff --git a/graphql/tarantool_graphql.lua b/graphql/tarantool_graphql.lua index fb579a5..2465d77 100644 --- a/graphql/tarantool_graphql.lua +++ b/graphql/tarantool_graphql.lua @@ -1617,64 +1617,67 @@ local function parse_cfg(cfg) return state end ---- The function just makes some reasonable assertions on input ---- and then call graphql-lua execute. -local function gql_execute(qstate, variables) +--- Execute an operation from compiled query. +--- +--- @tparam qstate compiled query +--- +--- @tparam variables variables to pass to the query +--- +--- @tparam[opt] string operation_name optional operation name +--- +--- @treturn table result of the operation +local function gql_execute(qstate, variables, operation_name) assert(qstate.state) local state = qstate.state assert(state.schema) - assert(type(variables) == 'table', 'variables must be table, got ' .. - type(variables)) + check(variables, 'variables', 'table') + check(operation_name, 'operation_name', 'string', 'nil') local root_value = {} - local operation_name = qstate.operation_name - assert(type(operation_name) == 'string', - 'operation_name must be a string, got ' .. type(operation_name)) return execute(state.schema, qstate.ast, root_value, variables, operation_name) end -local function compile_and_execute(state, query, variables) +--- Compile a query and execute an operation. +--- +--- See @{gql_compile} and @{gql_execute} for parameters description. +--- +--- @treturn table result of the operation +local function compile_and_execute(state, query, variables, operation_name) assert(type(state) == 'table', 'use :gql_execute(...) instead of ' .. '.execute(...)') + assert(state.schema ~= nil, 'have not compiled schema') check(query, 'query', 'string') check(variables, 'variables', 'table', 'nil') + check(operation_name, 'operation_name', 'string', 'nil') + local compiled_query = state:compile(query) - return compiled_query:execute(variables) + return compiled_query:execute(variables, operation_name) end ---- The function parses a query string, validate the resulting query ---- against the GraphQL schema and provides an object with the function to ---- execute the query with specific variables values. +--- Parse GraphQL query string, validate against the GraphQL schema and +--- provide an object with the function to execute an operation from the +--- request with specific variables values. --- ---- @tparam table state current state of graphql, including ---- schemas, collections and accessor ---- @tparam string query query string +--- @tparam table state a tarantool_graphql instance +--- +--- @tparam string query text of a GraphQL query +--- +--- @treturn table compiled query with `execute` and `avro_schema` functions local function gql_compile(state, query) assert(type(state) == 'table' and type(query) == 'string', 'use :validate(...) instead of .validate(...)') assert(state.schema ~= nil, 'have not compiled schema') + check(query, 'query', 'string') local ast = parse(query) - - local operation_name - for _, definition in pairs(ast.definitions) do - if definition.kind == 'operation' then - operation_name = definition.name.value - end - end - - assert(operation_name, "there is no 'operation' in query " .. - "definitions:\n" .. yaml.encode(ast)) - validate(state.schema, ast) local qstate = { state = state, ast = ast, - operation_name = operation_name, } local gql_query = setmetatable(qstate, { @@ -1719,11 +1722,11 @@ function tarantool_graphql.compile(query) return default_instance:compile(query) end -function tarantool_graphql.execute(query, variables) +function tarantool_graphql.execute(query, variables, operation_name) if default_instance == nil then default_instance = tarantool_graphql.new() end - return default_instance:execute(query, variables) + return default_instance:execute(query, variables, operation_name) end function tarantool_graphql.start_server() diff --git a/test/testdata/common_testdata.lua b/test/testdata/common_testdata.lua index 4c568f1..88a952c 100644 --- a/test/testdata/common_testdata.lua +++ b/test/testdata/common_testdata.lua @@ -311,7 +311,7 @@ end function common_testdata.run_queries(gql_wrapper) local test = tap.test('common') - test:plan(18) + test:plan(24) local query_1 = [[ query user_by_order($order_id: String) { @@ -338,13 +338,116 @@ function common_testdata.run_queries(gql_wrapper) first_name: Ivan ]]):strip()) + local variables_1 = {order_id = 'order_id_1'} + utils.show_trace(function() - local variables_1 = {order_id = 'order_id_1'} local gql_query_1 = gql_wrapper:compile(query_1) local result = gql_query_1:execute(variables_1) test:is_deeply(result, exp_result_1, '1') end) + local query_1n = [[ + query($order_id: String) { + order_collection(order_id: $order_id) { + order_id + description + user_connection { + user_id + last_name + first_name + } + } + } + ]] + + utils.show_trace(function() + local gql_query_1n = gql_wrapper:compile(query_1n) + local result = gql_query_1n:execute(variables_1) + test:is_deeply(result, exp_result_1, '1n') + end) + + local query_1inn = [[ + { + order_collection(order_id: "order_id_1") { + order_id + description + user_connection { + user_id + last_name + first_name + } + } + } + ]] + + utils.show_trace(function() + local gql_query_1inn = gql_wrapper:compile(query_1inn) + local result = gql_query_1inn:execute({}) + test:is_deeply(result, exp_result_1, '1inn') + end) + + local query_1tn = [[ + query get_order { + order_collection(order_id: "order_id_1") { + order_id + description + } + } + query { + order_collection(order_id: "order_id_1") { + order_id + description + } + } + ]] + + local err_exp = 'Cannot have more than one operation when using ' .. + 'anonymous operations' + local ok, err = pcall(gql_wrapper.compile, gql_wrapper, query_1tn) + test:is_deeply({ok, test_utils.strip_error(err)}, {false, err_exp}, + 'unnamed query should be a single one') + + local query_1t = [[ + query user_by_order { + order_collection(order_id: "order_id_1") { + order_id + description + user_connection { + user_id + last_name + first_name + } + } + } + query get_order { + order_collection(order_id: "order_id_1") { + order_id + description + } + } + ]] + + local gql_query_1t = utils.show_trace(function() + return gql_wrapper:compile(query_1t) + end) + + local err_exp = 'Operation name must be specified if more than one ' .. + 'operation exists.' + local ok, err = pcall(gql_query_1t.execute, gql_query_1t, {}) + test:is_deeply({ok, test_utils.strip_error(err)}, {false, err_exp}, + 'non-determined query name should give an error') + + local err_exp = 'Unknown operation "non_existent_operation"' + local ok, err = pcall(gql_query_1t.execute, gql_query_1t, {}, + 'non_existent_operation') + test:is_deeply({ok, test_utils.strip_error(err)}, {false, err_exp}, + 'wrong operation name should give an error') + + utils.show_trace(function() + local result = gql_query_1t:execute({}, 'user_by_order') + test:is_deeply(result, exp_result_1, 'execute an operation by name') + end) + local query_2 = [[ query user_order($user_id: String, $first_name: String, $limit: Int, $offset: String) {