From 49342c3982cbe27667cb124a7a652ee693ca817a Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 12:49:57 +0300 Subject: [PATCH 1/7] Add note on how to run specific test --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ac56f9d..5ce5945 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,10 @@ git clone https://github.com/tarantool/graphql.git git submodule update --recursive --init make test ``` +To run specific test: +``` +TEST_RUN_TESTS=common/mutation make test +``` ## Requirements From a3448228b82e725f96ded88838e413b1991cc4ca Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 12:51:10 +0300 Subject: [PATCH 2/7] Fix wording of two assertions --- graphql/impl.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphql/impl.lua b/graphql/impl.lua index 7ff6deb..d839c30 100644 --- a/graphql/impl.lua +++ b/graphql/impl.lua @@ -73,8 +73,8 @@ end --- @treturn table result of the operation local function compile_and_execute(state, query, variables, operation_name, opts) - assert(type(state) == 'table', 'use :gql_execute(...) instead of ' .. - '.execute(...)') + assert(type(state) == 'table', 'use :compile_and_execute(...) ' .. + 'instead of .compile_and_execute(...)') assert(state.schema ~= nil, 'have not compiled schema') check(query, 'query', 'string') check(variables, 'variables', 'table', 'nil') @@ -103,7 +103,7 @@ end --- @treturn table compiled query with `execute` and `avro_schema` functions local function gql_compile(state, query, opts) assert(type(state) == 'table' and type(query) == 'string', - 'use :validate(...) instead of .validate(...)') + 'use :gql_compile(...) instead of .gql_compile(...)') assert(state.schema ~= nil, 'have not compiled schema') check(query, 'query', 'string') check(opts, 'opts', 'table', 'nil') From 038c1eba9c668b1759d0254b343227ee6e1156b8 Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 13:06:58 +0300 Subject: [PATCH 3/7] Add creation of avro-schema from query with Maps Close #198 --- graphql/convert_schema/types.lua | 10 +- graphql/core/types.lua | 31 ++- graphql/query_to_avro.lua | 14 +- test/common/introspection.test.lua | 312 ++++++++++++++--------------- 4 files changed, 195 insertions(+), 172 deletions(-) diff --git a/graphql/convert_schema/types.lua b/graphql/convert_schema/types.lua index dac999a..7b4dcc3 100644 --- a/graphql/convert_schema/types.lua +++ b/graphql/convert_schema/types.lua @@ -448,10 +448,12 @@ function types.convert(state, avro_schema, opts) 'got %s (avro_schema %s)'):format(type(avro_schema.values), json.encode(avro_schema))) - -- validate avro schema format inside 'values' - types.convert(state, avro_schema.values, {context = context}) - - local res = core_types.map + table.insert(context.path, 'Map') + local converted_values = types.convert(state, avro_schema.values, + {context = context}) + table.remove(context.path, #context.path) + local map_name = helpers.full_name('Map', context) + local res = core_types.map({values = converted_values, name = map_name}) return avro_t == 'map' and core_types.nonNull(res) or res elseif avro_t == 'union' then return union.convert(avro_schema, { diff --git a/graphql/core/types.lua b/graphql/core/types.lua index 31999b7..683ac31 100644 --- a/graphql/core/types.lua +++ b/graphql/core/types.lua @@ -191,17 +191,26 @@ function types.union(config) return instance end -types.map = types.scalar({ - name = 'Map', - description = 'Map is a dictionary with string keys and values of ' .. - 'arbitrary but same among all values type', - serialize = function(value) return value end, - parseValue = function(value) return value end, - parseLiteral = function(node) - error('Literal parsing is implemented in util.coerceValue; ' .. - 'we should not go here') - end, -}) +function types.map(config) + local instance = { + __type = 'Scalar', + subtype = 'Map', + name = config.name, + description = 'Map is a dictionary with string keys and values of ' .. + 'arbitrary but same among all values type', + serialize = function(value) return value end, + parseValue = function(value) return value end, + parseLiteral = function(node) + error('Literal parsing is implemented in util.coerceValue; ' .. + 'we should not go here') + end, + values = config.values, + } + + instance.nonNull = types.nonNull(instance) + + return instance +end function types.inputObject(config) assert(type(config.name) == 'string', 'type name must be provided as a string') diff --git a/graphql/query_to_avro.lua b/graphql/query_to_avro.lua index a219bbd..78e79c5 100644 --- a/graphql/query_to_avro.lua +++ b/graphql/query_to_avro.lua @@ -16,6 +16,7 @@ local query_to_avro = {} -- forward declaration local object_to_avro +local map_to_avro local gql_scalar_to_avro_index = { String = "string", @@ -29,7 +30,9 @@ local gql_scalar_to_avro_index = { local function gql_scalar_to_avro(fieldType) assert(fieldType.__type == "Scalar", "GraphQL scalar field expected") - assert(fieldType.name ~= "Map", "Map type is not supported") + if fieldType.subtype == "Map" then + return map_to_avro(fieldType) + end local result = gql_scalar_to_avro_index[fieldType.name] assert(result ~= nil, "Unexpected scalar type: " .. fieldType.name) return result @@ -85,6 +88,15 @@ local function gql_type_to_avro(fieldType, subSelections, context) return result end +--- The function converts a GraphQL Map type to avro-schema map type. +map_to_avro = function(mapType) + assert(mapType.values ~= nil, "GraphQL Map type must have 'values' field") + return { + type = "map", + values = gql_type_to_avro(mapType.values), + } +end + --- The function converts a single Object field to avro format. local function field_to_avro(object_type, fields, context) local firstField = fields[1] diff --git a/test/common/introspection.test.lua b/test/common/introspection.test.lua index 35102c3..2da61d6 100755 --- a/test/common/introspection.test.lua +++ b/test/common/introspection.test.lua @@ -712,13 +712,13 @@ local function run_queries(gql_wrapper) name: String kind: SCALAR name: defaultValue - description: A GraphQL-formatted string representing the default value for - this input value. + description: A GraphQL-formatted string representing the default value for this + input value. kind: OBJECT name: __InputValue - description: Arguments provided to Fields or Directives and the input fields - of an InputObject are represented as Input Values which describe their type - and optionally a default value. + description: Arguments provided to Fields or Directives and the input fields of + an InputObject are represented as Input Values which describe their type and + optionally a default value. - kind: INPUT_OBJECT inputFields: - type: @@ -783,51 +783,10 @@ local function run_queries(gql_wrapper) name: parametrized_tags name: arguments___order_metainfo_collection___insert___order_metainfo_collection_insert___store___store description: generated from avro-schema for store - - interfaces: *0 - fields: - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: order_metainfo_id - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: order_metainfo_id_copy - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: metainfo - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: order_id - - isDeprecated: false - args: *0 - type: - ofType: - name: order_metainfo_collection___store___store - kind: OBJECT - kind: NON_NULL - name: store - kind: OBJECT - name: order_metainfo_collection - description: generated from avro-schema for order_metainfo + - kind: SCALAR + name: order_metainfo_collection___store___store___parametrized_tags___Map + description: Map is a dictionary with string keys and values of arbitrary but + same among all values type - kind: INPUT_OBJECT inputFields: - type: @@ -955,7 +914,7 @@ local function run_queries(gql_wrapper) args: *0 type: ofType: - name: Map + name: order_metainfo_collection___store___store___parametrized_tags___Map kind: SCALAR kind: NON_NULL name: parametrized_tags @@ -1223,9 +1182,9 @@ local function run_queries(gql_wrapper) description: A list of all directives supported by this server. kind: OBJECT name: __Schema - description: A GraphQL Schema defines the capabilities of a GraphQL server. - It exposes all available types and directives on the server, as well as the - entry points for query and mutation operations. + description: A GraphQL Schema defines the capabilities of a GraphQL server. It + exposes all available types and directives on the server, as well as the entry + points for query and mutation operations. - kind: INPUT_OBJECT inputFields: - type: @@ -1353,8 +1312,8 @@ local function run_queries(gql_wrapper) kind: OBJECT name: __EnumValue description: One possible value for a given Enum. Enum values are unique values, - not a placeholder for a string or numeric value. However an Enum value is - returned in a JSON response as a string. + not a placeholder for a string or numeric value. However an Enum value is returned + in a JSON response as a string. - interfaces: *0 fields: - isDeprecated: false @@ -1449,9 +1408,9 @@ local function run_queries(gql_wrapper) many kinds of types in GraphQL as represented by the `__TypeKind` enum. Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum - types provide their values. Object and Interface types provide the fields - they describe. Abstract types, Union and Interface, provide the Object types - possible at runtime. List and NonNull types compose other types. + types provide their values. Object and Interface types provide the fields they + describe. Abstract types, Union and Interface, provide the Object types possible + at runtime. List and NonNull types compose other types. - kind: INPUT_OBJECT inputFields: - type: @@ -1510,7 +1469,7 @@ local function run_queries(gql_wrapper) name: String kind: SCALAR kind: NON_NULL - name: state + name: order_metainfo_id - isDeprecated: false args: *0 type: @@ -1518,7 +1477,7 @@ local function run_queries(gql_wrapper) name: String kind: SCALAR kind: NON_NULL - name: zip + name: order_metainfo_id_copy - isDeprecated: false args: *0 type: @@ -1526,7 +1485,7 @@ local function run_queries(gql_wrapper) name: String kind: SCALAR kind: NON_NULL - name: city + name: metainfo - isDeprecated: false args: *0 type: @@ -1534,10 +1493,18 @@ local function run_queries(gql_wrapper) name: String kind: SCALAR kind: NON_NULL - name: street + name: order_id + - isDeprecated: false + args: *0 + type: + ofType: + name: order_metainfo_collection___store___store + kind: OBJECT + kind: NON_NULL + name: store kind: OBJECT - name: order_metainfo_collection___store___store___address___address - description: generated from avro-schema for address + name: order_metainfo_collection + description: generated from avro-schema for order_metainfo - kind: INPUT_OBJECT inputFields: - type: @@ -1720,10 +1687,13 @@ local function run_queries(gql_wrapper) description: The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. - - kind: SCALAR - name: Map - description: Map is a dictionary with string keys and values of arbitrary but - same among all values type + - possibleTypes: + - name: Int_box + kind: OBJECT + - name: String_box + kind: OBJECT + name: order_metainfo_collection___store___store___external_id___external_id + kind: UNION - kind: INPUT_OBJECT inputFields: - type: @@ -1813,13 +1783,43 @@ local function run_queries(gql_wrapper) name: string name: arguments___order_metainfo_collection___update___order_metainfo_collection_update___store___store___external_id___external_id___String_box description: Box (wrapper) around union variant - - possibleTypes: - - name: Int_box - kind: OBJECT - - name: String_box - kind: OBJECT - name: order_metainfo_collection___store___store___external_id___external_id - kind: UNION + - interfaces: *0 + fields: + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: state + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: zip + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: city + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: street + kind: OBJECT + name: order_metainfo_collection___store___store___address___address + description: generated from avro-schema for address - kind: INPUT_OBJECT inputFields: - type: @@ -2314,31 +2314,10 @@ local function run_queries(gql_wrapper) name: street name: arguments___order_metainfo_collection___store___store___second_address___address description: generated from avro-schema for address - - kind: INPUT_OBJECT - inputFields: - - type: - name: String - kind: SCALAR - name: order_metainfo_id - - type: - name: String - kind: SCALAR - name: order_metainfo_id_copy - - type: - name: String - kind: SCALAR - name: metainfo - - type: - name: String - kind: SCALAR - name: order_id - - type: - name: arguments___order_metainfo_collection___store___store - kind: INPUT_OBJECT - name: store - name: order_metainfo_connection - description: generated from the connection "order_metainfo_connection" of collection - "order_collection" using collection "order_metainfo_collection" + - kind: SCALAR + name: order_metainfo_collection___store___store___parametrized_tags___Map + description: Map is a dictionary with string keys and values of arbitrary but + same among all values type - name: Float kind: SCALAR - interfaces: *0 @@ -2404,7 +2383,7 @@ local function run_queries(gql_wrapper) args: *0 type: ofType: - name: Map + name: order_metainfo_collection___store___store___parametrized_tags___Map kind: SCALAR kind: NON_NULL name: parametrized_tags @@ -2660,9 +2639,9 @@ local function run_queries(gql_wrapper) description: A list of all directives supported by this server. kind: OBJECT name: __Schema - description: A GraphQL Schema defines the capabilities of a GraphQL server. - It exposes all available types and directives on the server, as well as the - entry points for query and mutation operations. + description: A GraphQL Schema defines the capabilities of a GraphQL server. It + exposes all available types and directives on the server, as well as the entry + points for query and mutation operations. - interfaces: *0 fields: - isDeprecated: false @@ -2696,8 +2675,8 @@ local function run_queries(gql_wrapper) kind: OBJECT name: __EnumValue description: One possible value for a given Enum. Enum values are unique values, - not a placeholder for a string or numeric value. However an Enum value is - returned in a JSON response as a string. + not a placeholder for a string or numeric value. However an Enum value is returned + in a JSON response as a string. - interfaces: *0 fields: - isDeprecated: false @@ -2792,9 +2771,9 @@ local function run_queries(gql_wrapper) many kinds of types in GraphQL as represented by the `__TypeKind` enum. Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum - types provide their values. Object and Interface types provide the fields - they describe. Abstract types, Union and Interface, provide the Object types - possible at runtime. List and NonNull types compose other types. + types provide their values. Object and Interface types provide the fields they + describe. Abstract types, Union and Interface, provide the Object types possible + at runtime. List and NonNull types compose other types. - kind: INPUT_OBJECT inputFields: - type: @@ -2849,13 +2828,13 @@ local function run_queries(gql_wrapper) name: String kind: SCALAR name: defaultValue - description: A GraphQL-formatted string representing the default value for - this input value. + description: A GraphQL-formatted string representing the default value for this + input value. kind: OBJECT name: __InputValue - description: Arguments provided to Fields or Directives and the input fields - of an InputObject are represented as Input Values which describe their type - and optionally a default value. + description: Arguments provided to Fields or Directives and the input fields of + an InputObject are represented as Input Values which describe their type and + optionally a default value. - interfaces: *0 fields: - isDeprecated: false @@ -2969,43 +2948,31 @@ local function run_queries(gql_wrapper) kind: OBJECT name: order_metainfo_collection description: generated from avro-schema for order_metainfo - - interfaces: *0 - fields: - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: state - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: zip - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: city - - isDeprecated: false - args: *0 - type: - ofType: - name: String - kind: SCALAR - kind: NON_NULL - name: street - kind: OBJECT - name: order_metainfo_collection___store___store___address___address - description: generated from avro-schema for address + - kind: INPUT_OBJECT + inputFields: + - type: + name: String + kind: SCALAR + name: order_metainfo_id + - type: + name: String + kind: SCALAR + name: order_metainfo_id_copy + - type: + name: String + kind: SCALAR + name: metainfo + - type: + name: String + kind: SCALAR + name: order_id + - type: + name: arguments___order_metainfo_collection___store___store + kind: INPUT_OBJECT + name: store + name: order_metainfo_connection + description: generated from the connection "order_metainfo_connection" of collection + "order_collection" using collection "order_metainfo_collection" - interfaces: *0 fields: - isDeprecated: false @@ -3107,6 +3074,43 @@ local function run_queries(gql_wrapper) kind: OBJECT name: order_collection description: generated from avro-schema for order + - interfaces: *0 + fields: + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: state + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: zip + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: city + - isDeprecated: false + args: *0 + type: + ofType: + name: String + kind: SCALAR + kind: NON_NULL + name: street + kind: OBJECT + name: order_metainfo_collection___store___store___address___address + description: generated from avro-schema for address - kind: INPUT_OBJECT inputFields: - type: @@ -3127,10 +3131,6 @@ local function run_queries(gql_wrapper) name: middle_name name: user_collection_pcre description: generated from avro-schema for user_collection_pcre - - kind: SCALAR - name: Map - description: Map is a dictionary with string keys and values of arbitrary but - same among all values type - kind: INPUT_OBJECT inputFields: - type: From 87df8fda00df8351699da6111401d252eaede159 Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 13:12:00 +0300 Subject: [PATCH 4/7] Fix problem with multihead connections with nulls Close #200 --- README.md | 9 + graphql/convert_schema/resolve.lua | 22 +- test/common/multihead_conn_with_nulls.lua | 16 + .../multihead_conn_with_nulls_testdata.lua | 467 ++++++++++++++++++ 4 files changed, 512 insertions(+), 2 deletions(-) create mode 100755 test/common/multihead_conn_with_nulls.lua create mode 100644 test/testdata/multihead_conn_with_nulls_testdata.lua diff --git a/README.md b/README.md index 5ce5945..7e7e90e 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ local compiled_query = graphql_lib.compile(query) local result = compiled_query:execute(variables) ``` +### Multi-head connections +A parent object is matching against a multi-head connection variants in the +order of the variants. The parent object should match with a determinant of +at least one variant except the following case. When source fields of all +variants are null the multi-head connection obligated to give null object as +the result. In this case the parent object is allowed to don’t match any variant. +One can use this feature to avoid to set any specific determinant value when a +multi-head connection is known to have no connected object. + ### Mutations Mutations are disabled for avro-schema-2\*, because it can work incorrectly for diff --git a/graphql/convert_schema/resolve.lua b/graphql/convert_schema/resolve.lua index cc33c7b..557d9bc 100644 --- a/graphql/convert_schema/resolve.lua +++ b/graphql/convert_schema/resolve.lua @@ -33,7 +33,7 @@ end --- --- Note that connection key parts can be prefix of index key parts. Zero parts --- count considered as ok by this check. -local function are_all_parts_null(parent, connection_parts) +local function are_all_parts_null(parent, connection_parts, opts) local are_all_parts_null = true local are_all_parts_non_null = true for _, part in ipairs(connection_parts) do @@ -47,7 +47,9 @@ local function are_all_parts_null(parent, connection_parts) end local ok = are_all_parts_null or are_all_parts_non_null - if not ok then -- avoid extra json.encode() + local opts = opts or {} + local no_assert = opts.no_assert or false + if not ok and not no_assert then -- avoid extra json.encode() assert(ok, 'FULL MATCH constraint was failed: connection ' .. 'key parts must be all non-nulls or all nulls; ' .. @@ -190,6 +192,22 @@ function resolve.gen_resolve_function_multihead(collection_name, connection, end return function(parent, _, info) + -- If a parent object does not have all source fields (for any of + -- variants) non-null then we do not resolve variant and just return + -- box.NULL. + local is_source_fields_found = false + for _, variant in ipairs(c.variants) do + is_source_fields_found = + not are_all_parts_null(parent, variant.parts, {no_assert = true}) + if is_source_fields_found then + break + end + end + + if not is_source_fields_found then + return box.NULL, nil + end + local v, variant_num, box_field_name = resolve_variant(parent) local destination_type = union_types[variant_num] diff --git a/test/common/multihead_conn_with_nulls.lua b/test/common/multihead_conn_with_nulls.lua new file mode 100755 index 0000000..9b33984 --- /dev/null +++ b/test/common/multihead_conn_with_nulls.lua @@ -0,0 +1,16 @@ +#!/usr/bin/env tarantool + +local fio = require('fio') + +-- 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 test_utils = require('test.test_utils') +local testdata = require('test.testdata.multihead_conn_with_nulls_testdata') + +box.cfg({}) + +test_utils.run_testdata(testdata) + +os.exit() diff --git a/test/testdata/multihead_conn_with_nulls_testdata.lua b/test/testdata/multihead_conn_with_nulls_testdata.lua new file mode 100644 index 0000000..f1edbe1 --- /dev/null +++ b/test/testdata/multihead_conn_with_nulls_testdata.lua @@ -0,0 +1,467 @@ +--- The difference between this testdata and multihead_conn_testdata.lua +--- is that in this testdata we have nullable source fields and nullable +--- determinant fields, while in multihead_conn_testdata.lua all these fields +--- are non-nullable. + +local tap = require('tap') +local json = require('json') +local yaml = require('yaml') +local test_utils = require('test.test_utils') + +local multihead_conn_testdata = {} + +function multihead_conn_testdata.get_test_metadata() + local schemas = json.decode([[{ + "hero": { + "name": "hero", + "type": "record", + "fields": [ + { "name": "hero_id", "type": "string" }, + { "name": "hero_subtype_id", "type": "string*" }, + { "name": "hero_type", "type": "string*" }, + { "name": "banking_id", "type" : "string*" }, + { "name": "banking_type", "type" : "string*" } + ] + }, + "human": { + "name": "human", + "type": "record", + "fields": [ + { "name": "human_id", "type": "string" }, + { "name": "name", "type": "string" }, + { "name": "episode", "type": "string"} + ] + }, + "starship": { + "name": "starship", + "type": "record", + "fields": [ + { "name": "starship_id", "type": "string" }, + { "name": "model", "type": "string" }, + { "name": "episode", "type": "string"} + ] + }, + "credit_account": { + "name": "credit_account", + "type": "record", + "fields": [ + { "name": "account_id", "type": "string" }, + { "name": "hero_banking_id", "type": "string" } + ] + }, + "dublon_account": { + "name": "dublon_account", + "type": "record", + "fields": [ + { "name": "account_id", "type": "string" }, + { "name": "hero_banking_id", "type": "string" } + ] + } + }]]) + + local collections = json.decode([[{ + "hero_collection": { + "schema_name": "hero", + "connections": [ + { + "name": "hero_connection", + "type": "1:1", + "variants": [ + { + "determinant": {"hero_type": "human"}, + "destination_collection": "human_collection", + "parts": [ + { + "source_field": "hero_subtype_id", + "destination_field": "human_id" + } + ], + "index_name": "human_id_index" + }, + { + "determinant": {"hero_type": "starship"}, + "destination_collection": "starship_collection", + "parts": [ + { + "source_field": "hero_subtype_id", + "destination_field": "starship_id" + } + ], + "index_name": "starship_id_index" + } + ] + }, + { + "name": "hero_banking_connection", + "type": "1:N", + "variants": [ + { + "determinant": {"banking_type": "credit"}, + "destination_collection": "credit_account_collection", + "parts": [ + { + "source_field": "banking_id", + "destination_field": "hero_banking_id" + } + ], + "index_name": "credit_hero_banking_id_index" + }, + { + "determinant": {"banking_type": "dublon"}, + "destination_collection": "dublon_account_collection", + "parts": [ + { + "source_field": "banking_id", + "destination_field": "hero_banking_id" + } + ], + "index_name": "dublon_hero_banking_id_index" + } + ] + } + ] + }, + "human_collection": { + "schema_name": "human", + "connections": [] + }, + "starship_collection": { + "schema_name": "starship", + "connections": [] + }, + "credit_account_collection": { + "schema_name": "credit_account", + "connections": [] + }, + "dublon_account_collection": { + "schema_name": "dublon_account", + "connections": [] + } + }]]) + + local service_fields = { + hero = { + { name = 'expires_on', type = 'long', default = 0 }, + }, + human = { + { name = 'expires_on', type = 'long', default = 0 }, + }, + starship = { + { name = 'expires_on', type = 'long', default = 0 }, + }, + + credit_account = { + { name = 'expires_on', type = 'long', default = 0 }, + }, + + dublon_account = { + { name = 'expires_on', type = 'long', default = 0 }, + } + } + + local indexes = { + hero_collection = { + hero_id_index = { + service_fields = {}, + fields = { 'hero_id' }, + index_type = 'tree', + unique = true, + primary = true, + }, + }, + + human_collection = { + human_id_index = { + service_fields = {}, + fields = { 'human_id' }, + index_type = 'tree', + unique = true, + primary = true, + }, + }, + + starship_collection = { + starship_id_index = { + service_fields = {}, + fields = { 'starship_id' }, + index_type = 'tree', + unique = true, + primary = true, + }, + }, + + credit_account_collection = { + credit_id_index = { + service_fields = {}, + fields = { 'account_id' }, + index_type = 'tree', + unique = true, + primary = true, + }, + credit_hero_banking_id_index = { + service_fields = {}, + fields = { 'hero_banking_id' }, + index_type = 'tree', + unique = false, + primary = false, + } + }, + + dublon_account_collection = { + dublon_id_index = { + service_fields = {}, + fields = { 'account_id' }, + index_type = 'tree', + unique = true, + primary = true, + }, + dublon_hero_banking_id_index = { + service_fields = {}, + fields = { 'hero_banking_id' }, + index_type = 'tree', + unique = false, + primary = false, + } + } + } + + return { + schemas = schemas, + collections = collections, + service_fields = service_fields, + indexes = indexes, + } +end + +function multihead_conn_testdata.init_spaces() + local HERO_ID_FIELD_NUM = 2 + local HERO_BANKING_ID_FIELD_NUM = 3 + + box.once('test_space_init_spaces', function() + box.schema.create_space('hero_collection') + box.space.hero_collection:create_index('hero_id_index', + { type = 'tree', unique = true, + parts = { HERO_ID_FIELD_NUM, 'string' }} + ) + + box.schema.create_space('human_collection') + box.space.human_collection:create_index('human_id_index', + { type = 'tree', unique = true, + parts = { HERO_ID_FIELD_NUM, 'string' }} + ) + + box.schema.create_space('starship_collection') + box.space.starship_collection:create_index('starship_id_index', + { type = 'tree', unique = true, + parts = { HERO_ID_FIELD_NUM, 'string' }} + ) + + box.schema.create_space('credit_account_collection') + box.space.credit_account_collection:create_index('credit_id_index', + { type = 'tree', unique = true, + parts = { HERO_ID_FIELD_NUM, 'string' }} + ) + box.space.credit_account_collection:create_index( + 'credit_hero_banking_id_index', + { type = 'tree', unique = false, + parts = { HERO_BANKING_ID_FIELD_NUM, 'string' }} + ) + + box.schema.create_space('dublon_account_collection') + box.space.dublon_account_collection:create_index('dublon_id_index', + { type = 'tree', unique = true, + parts = { HERO_ID_FIELD_NUM, 'string' }} + ) + box.space.dublon_account_collection:create_index( + 'dublon_hero_banking_id_index', + { type = 'tree', unique = false, + parts = { HERO_BANKING_ID_FIELD_NUM, 'string' }} + ) + end) +end + +function multihead_conn_testdata.fill_test_data(shard, meta) + local shard = shard or box.space + + test_utils.replace_object(shard, meta, 'hero_collection', { + hero_id = "hero_id_1", + hero_subtype_id = "human_id_1", + hero_type = "human", + banking_type = "credit", + banking_id = "hero_banking_id_1" + }, { + 1827767717 + }) + + test_utils.replace_object(shard, meta, 'hero_collection', { + hero_id = "hero_id_2", + hero_subtype_id = "starship_id_1", + hero_type = "starship", + banking_type = "dublon", + banking_id = "hero_banking_id_2" + }, { + 1827767717 + }) + + test_utils.replace_object(shard, meta, 'hero_collection', { + hero_id = "hero_id_3", + hero_subtype_id = box.NULL, + hero_type = box.NULL, + banking_type = box.NULL, + banking_id = box.NULL + }, { + 1827767717 + }) + + test_utils.replace_object(shard, meta, 'human_collection', { + human_id = "human_id_1", + name = "Luke", + episode = "EMPR" + }, { + 1827767717 + }) + + test_utils.replace_object(shard, meta, 'starship_collection', { + starship_id = "starship_id_1", + model = "Falcon-42", + episode = "NEW" + }, { + 1827767717 + }) + + for _, i in ipairs({"1", "2", "3"}) do + test_utils.replace_object(shard, meta, 'credit_account_collection', { + account_id = "credit_account_id_" .. i, + hero_banking_id = "hero_banking_id_1" + }, { + 1827767717 + }) + + test_utils.replace_object(shard, meta, 'dublon_account_collection', { + account_id = "dublon_account_id_" .. i, + hero_banking_id = "hero_banking_id_2" + }, { + 1827767717 + }) + end +end + +function multihead_conn_testdata.drop_spaces() + box.space._schema:delete('oncetest_space_init_spaces') + box.space.human_collection:drop() + box.space.starship_collection:drop() + box.space.hero_collection:drop() + box.space.credit_account_collection:drop() + box.space.dublon_account_collection:drop() +end + +function multihead_conn_testdata.run_queries(gql_wrapper) + local test = tap.test('multihead_conn_null') + test:plan(3) + + local query = [[ + query obtainHeroes($hero_id: String) { + hero_collection(hero_id: $hero_id) { + hero_id + hero_type + hero_connection { + ... on box_human_collection { + human_collection { + name + } + } + ... on box_starship_collection { + starship_collection { + model + } + } + } + banking_type + hero_banking_connection { + ... on box_array_credit_account_collection { + credit_account_collection { + account_id + hero_banking_id + } + } + ... on box_array_dublon_account_collection { + dublon_account_collection { + account_id + hero_banking_id + } + } + } + } + } + ]] + + local gql_query_1 = test_utils.show_trace(function() + return gql_wrapper:compile(query) + end) + + local variables_1_1 = {hero_id = 'hero_id_1'} + local result_1_1 = test_utils.show_trace(function() + return gql_query_1:execute(variables_1_1) + end) + local exp_result_1_1 = yaml.decode(([[ + hero_collection: + - hero_id: hero_id_1 + hero_type: human + hero_connection: + human_collection: + name: Luke + banking_type: credit + hero_banking_connection: + credit_account_collection: + - account_id: credit_account_id_1 + hero_banking_id: hero_banking_id_1 + - account_id: credit_account_id_2 + hero_banking_id: hero_banking_id_1 + - account_id: credit_account_id_3 + hero_banking_id: hero_banking_id_1 + ]]):strip()) + test:is_deeply(result_1_1.data, exp_result_1_1, '1_1') + + local variables_1_2 = {hero_id = 'hero_id_2'} + local result_1_2 = test_utils.show_trace(function() + return gql_query_1:execute(variables_1_2) + end) + local exp_result_1_2 = yaml.decode(([[ + hero_collection: + - hero_id: hero_id_2 + hero_type: starship + hero_connection: + starship_collection: + model: Falcon-42 + banking_type: dublon + hero_banking_connection: + dublon_account_collection: + - account_id: dublon_account_id_1 + hero_banking_id: hero_banking_id_2 + - account_id: dublon_account_id_2 + hero_banking_id: hero_banking_id_2 + - account_id: dublon_account_id_3 + hero_banking_id: hero_banking_id_2 + ]]):strip()) + test:is_deeply(result_1_2.data, exp_result_1_2, '1_2') + + --- 'hero' with 'hero_id' = 'hero_id_3' has null 'hero_subtype_id', + --- 'hero_type', 'banking_id' and 'banking_type'. Below we test that + --- everything works as expected at such case. + --- Also we check that this problem + --- (https://github.com/tarantool/graphql/issues/200) does not appear + --- anymore. + local variables_1_3 = {hero_id = 'hero_id_3'} + local result_1_3 = test_utils.show_trace(function() + return gql_query_1:execute(variables_1_3) + end) + local exp_result_1_3 = yaml.decode(([[ + hero_collection: + - hero_id: hero_id_3 + ]]):strip()) + test:is_deeply(result_1_3.data, exp_result_1_3, '1_3') + + assert(test:check(), 'check plan') +end + +return multihead_conn_testdata From b2616d882e980e40fd6a8a01a680d6a6d3d53f5f Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 13:13:15 +0300 Subject: [PATCH 5/7] Make small clean up --- test/testdata/multihead_conn_testdata.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/testdata/multihead_conn_testdata.lua b/test/testdata/multihead_conn_testdata.lua index 0108fa6..6545de2 100644 --- a/test/testdata/multihead_conn_testdata.lua +++ b/test/testdata/multihead_conn_testdata.lua @@ -228,7 +228,7 @@ end function multihead_conn_testdata.init_spaces() local ID_FIELD_NUM = 2 - local HERO_ID_FIELD_NUM =3 + local HERO_ID_FIELD_NUM = 3 box.once('test_space_init_spaces', function() box.schema.create_space('hero_collection') @@ -251,7 +251,8 @@ function multihead_conn_testdata.init_spaces() { type = 'tree', unique = true, parts = { ID_FIELD_NUM, 'string' }} ) box.space.credit_account_collection:create_index('credit_hero_id_index', - { type = 'tree', unique = false, parts = { HERO_ID_FIELD_NUM, 'string' }} + { type = 'tree', unique = false, + parts = { HERO_ID_FIELD_NUM, 'string' }} ) box.schema.create_space('dublon_account_collection') @@ -259,7 +260,8 @@ function multihead_conn_testdata.init_spaces() { type = 'tree', unique = true, parts = { ID_FIELD_NUM, 'string' }} ) box.space.dublon_account_collection:create_index('dublon_hero_id_index', - { type = 'tree', unique = false, parts = { HERO_ID_FIELD_NUM, 'string' }} + { type = 'tree', unique = false, + parts = { HERO_ID_FIELD_NUM, 'string' }} ) end) end @@ -346,14 +348,12 @@ function multihead_conn_testdata.run_queries(gql_wrapper) hero_id } } - ... on box_array_dublon_account_collection { dublon_account_collection { account_id hero_id } } - } } } From ce7facadafea08367a180ebe8170cb8df1aa770d Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 13:31:12 +0300 Subject: [PATCH 6/7] Add creation of avro-schema from query with Unions and multihead connections Close #197, close #84 --- graphql/query_to_avro.lua | 95 +++++++- test/extra/to_avro_multihead.test.lua | 231 ++++++++++++++++++++ test/extra/to_avro_unions_and_maps.test.lua | 152 +++++++++++++ 3 files changed, 476 insertions(+), 2 deletions(-) create mode 100755 test/extra/to_avro_multihead.test.lua create mode 100755 test/extra/to_avro_unions_and_maps.test.lua diff --git a/graphql/query_to_avro.lua b/graphql/query_to_avro.lua index 78e79c5..e4b4958 100644 --- a/graphql/query_to_avro.lua +++ b/graphql/query_to_avro.lua @@ -10,6 +10,8 @@ local introspection = require(path .. '.introspection') local query_util = require(path .. '.query_util') local avro_helpers = require('graphql.avro_helpers') local convert_schema_helpers = require('graphql.convert_schema.helpers') +local utils = require('graphql.utils') +local check = utils.check -- module functions local query_to_avro = {} @@ -17,6 +19,7 @@ local query_to_avro = {} -- forward declaration local object_to_avro local map_to_avro +local union_to_avro local gql_scalar_to_avro_index = { String = "string", @@ -74,8 +77,10 @@ local function gql_type_to_avro(fieldType, subSelections, context) result = gql_scalar_to_avro(fieldType) elseif fieldTypeName == 'Object' then result = object_to_avro(fieldType, subSelections, context) - elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then - error('Interfaces and Unions are not supported yet') + elseif fieldTypeName == 'Union' then + result = union_to_avro(fieldType, subSelections, context) + elseif fieldTypeName == 'Interface' then + error('Interfaces are not supported yet') else error(string.format('Unknown type "%s"', tostring(fieldTypeName))) end @@ -97,6 +102,92 @@ map_to_avro = function(mapType) } end +--- Converts a GraphQL Union type to avro-schema type. +--- +--- Currently we use GraphQL Unions to implement both multi-head connections +--- and avro-schema unions. The function distinguishes between them relying on +--- 'fieldType.resolveType'. GraphQL Union implementing multi-head +--- connection does not have such field, as it has another mechanism of union +--- type resolving. +--- +--- We have to distinguish between these two types of GraphQL Unions because +--- we want to create different avro-schemas for them. +--- +--- GraphQL Unions implementing avro-schema unions are to be converted back +--- to avro-schema unions. +--- +--- GraphQL Unions implementing multi-head connections are to be converted to +--- avro-schema records. Each field represents one union variant. Variant type +--- name is taken as a field name. Such records must have all fields nullable. +--- +--- We convert Unions implementing multi-head connections to records instead of +--- unions because in case of 1:N connections we would not have valid +--- avro-schema (if use unions). Avro-schema unions may not contain more than +--- one schema with the same non-named type (in case of 1:N multi-head +--- connections we would have more than one 'array' in union). +union_to_avro = function(fieldType, subSelections, context) + assert(fieldType.types ~= nil, "GraphQL Union must have 'types' field") + check(fieldType.types, "fieldType.types", "table") + local is_multihead = (fieldType.resolveType == nil) + local result + + if is_multihead then + check(fieldType.name, "fieldType.name", "string") + result = { + type = 'record', + name = fieldType.name, + fields = {} + } + else + result = {} + end + + for _, box_type in ipairs(fieldType.types) do + -- In GraphQL schema all types in Unions are 'boxed'. Here we + -- 'Unbox' types and selectionSets. More info on 'boxing' can be + -- found at @{convert_schema.types.convert_multihead_connection} + -- and at @{convert_schema.union}. + check(box_type, "box_type", "table") + assert(box_type.__type == "Object", "Box type must be a GraphQL Object") + assert(utils.table_size(box_type.fields) == 1, 'Box Object must ' .. + 'have exactly one field') + local type = select(2, next(box_type.fields)) + + local box_sub_selections + for _, s in pairs(subSelections) do + if s.typeCondition.name.value == box_type.name then + box_sub_selections = s + break + end + end + assert(box_sub_selections ~= nil) + + -- We have to extract subSelections from 'box' type. + local type_sub_selections + if box_sub_selections.selectionSet.selections[1].selectionSet ~= nil then + -- Object GraphQL type case. + type_sub_selections = box_sub_selections.selectionSet + .selections[1].selectionSet.selections + else + -- Scalar GraphQL type case. + type_sub_selections = box_sub_selections.selectionSet.selections[1] + end + assert(type_sub_selections ~= nil) + + if is_multihead then + local avro_type = gql_type_to_avro(type.kind, + type_sub_selections, context) + avro_type = avro_helpers.make_avro_type_nullable(avro_type) + table.insert(result.fields, {name = type.name, type = avro_type}) + else + table.insert(result, gql_type_to_avro(type.kind, + type_sub_selections, context)) + end + end + + return result +end + --- The function converts a single Object field to avro format. local function field_to_avro(object_type, fields, context) local firstField = fields[1] diff --git a/test/extra/to_avro_multihead.test.lua b/test/extra/to_avro_multihead.test.lua new file mode 100755 index 0000000..2710c81 --- /dev/null +++ b/test/extra/to_avro_multihead.test.lua @@ -0,0 +1,231 @@ +#!/usr/bin/env tarantool +local fio = require('fio') +local yaml = require('yaml') +local avro = require('avro_schema') +local test = require('tap').test('to avro schema') +local testdata = require('test.testdata.multihead_conn_with_nulls_testdata') +local graphql = require('graphql') + +-- 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 + +test:plan(7) + +box.cfg{} + +testdata.init_spaces() +local meta = testdata.get_test_metadata() +testdata.fill_test_data(box.space, meta) + +local gql_wrapper = graphql.new({ + schemas = meta.schemas, + collections = meta.collections, + service_fields = meta.service_fields, + indexes = meta.indexes, + accessor = 'space' +}) + +local query = [[ + query obtainHeroes($hero_id: String) { + hero_collection(hero_id: $hero_id) { + hero_id + hero_type + banking_type + hero_connection { + ... on box_human_collection { + human_collection { + name + } + } + ... on box_starship_collection { + starship_collection { + model + } + } + } + hero_banking_connection { + ... on box_array_credit_account_collection { + credit_account_collection { + account_id + hero_banking_id + } + } + ... on box_array_dublon_account_collection { + dublon_account_collection { + account_id + hero_banking_id + } + } + } + } + } +]] + +local expected_avro_schema = [[ + type: record + name: Query + fields: + - name: hero_collection + type: + type: array + items: + type: record + fields: + - name: hero_id + type: string + - name: hero_type + type: string* + - name: banking_type + type: string* + - name: hero_connection + type: + type: record* + name: hero_connection + fields: + - name: human_collection + type: + type: record* + fields: + - name: name + type: string + name: human_collection + namespace: Query.hero_collection + - name: starship_collection + type: + type: record* + fields: + - name: model + type: string + name: starship_collection + namespace: Query.hero_collection + - name: hero_banking_connection + type: + type: record* + name: hero_banking_connection + fields: + - name: credit_account_collection + type: + type: array* + items: + type: record + fields: + - name: account_id + type: string + - name: hero_banking_id + type: string + name: credit_account_collection + namespace: Query.hero_collection + - name: dublon_account_collection + type: + type: array* + items: + type: record + fields: + - name: account_id + type: string + - name: hero_banking_id + type: string + name: dublon_account_collection + namespace: Query.hero_collection + name: hero_collection + namespace: Query +]] + +local gql_query = gql_wrapper:compile(query) +local avro_from_query = gql_query:avro_schema() + +expected_avro_schema = yaml.decode(expected_avro_schema) + +test:is_deeply(avro_from_query, expected_avro_schema, + 'comparision between expected and generated (from query) avro-schemas') + +local ok, compiled_schema = avro.create(avro_from_query) +assert(ok, tostring(compiled_schema)) + +local variables_1 = { + hero_id = 'hero_id_1' +} + +local result_1 = gql_query:execute(variables_1) +result_1 = result_1.data +local result_expected_1 = yaml.decode([[ + hero_collection: + - hero_id: hero_id_1 + hero_type: human + hero_connection: + human_collection: + name: Luke + banking_type: credit + hero_banking_connection: + credit_account_collection: + - account_id: credit_account_id_1 + hero_banking_id: hero_banking_id_1 + - account_id: credit_account_id_2 + hero_banking_id: hero_banking_id_1 + - account_id: credit_account_id_3 + hero_banking_id: hero_banking_id_1 +]]) + +test:is_deeply(result_1, result_expected_1, + 'comparision between expected and actual query response 1') + +local ok, err = avro.validate(compiled_schema, result_1) +assert(ok, tostring(err)) +test:is(ok, true, 'query response validation by avro 1') + +local variables_2 = { + hero_id = 'hero_id_2' +} + +local result_2 = gql_query:execute(variables_2) +result_2 = result_2.data +local result_expected_2 = yaml.decode([[ + hero_collection: + - hero_id: hero_id_2 + hero_type: starship + hero_connection: + starship_collection: + model: Falcon-42 + banking_type: dublon + hero_banking_connection: + dublon_account_collection: + - account_id: dublon_account_id_1 + hero_banking_id: hero_banking_id_2 + - account_id: dublon_account_id_2 + hero_banking_id: hero_banking_id_2 + - account_id: dublon_account_id_3 + hero_banking_id: hero_banking_id_2 +]]) + +test:is_deeply(result_2, result_expected_2, + 'comparision between expected and actual query response 2') + +local ok, err = avro.validate(compiled_schema, result_2) +assert(ok, tostring(err)) +test:is(ok, true, 'query response validation by avro 2') + +local variables_3 = { + hero_id = 'hero_id_3' +} + +local result_3 = gql_query:execute(variables_3) +result_3 = result_3.data + +local result_expected_3 = yaml.decode([[ + hero_collection: + - hero_id: hero_id_3 +]]) + +test:is_deeply(result_3, result_expected_3, + 'comparision between expected and actual query response 3') + +local ok, err = avro.validate(compiled_schema, result_3) +assert(ok, tostring(err)) +test:is(ok, true, 'query response validation by avro 3') + +testdata.drop_spaces() + +assert(test:check(), 'check plan') + +os.exit() diff --git a/test/extra/to_avro_unions_and_maps.test.lua b/test/extra/to_avro_unions_and_maps.test.lua new file mode 100755 index 0000000..84ac2e5 --- /dev/null +++ b/test/extra/to_avro_unions_and_maps.test.lua @@ -0,0 +1,152 @@ +#!/usr/bin/env tarantool +local fio = require('fio') +local yaml = require('yaml') +local avro = require('avro_schema') +local test = require('tap').test('to avro schema') + +-- 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 testdata = require('test.testdata.union_testdata') + +local graphql = require('graphql') + +box.cfg{wal_mode="none"} +test:plan(3) + +testdata.init_spaces() +testdata.fill_test_data() +local meta = testdata.get_test_metadata() + +local gql_wrapper = graphql.new({ + schemas = meta.schemas, + collections = meta.collections, + service_fields = meta.service_fields, + indexes = meta.indexes, + accessor = 'space' +}) + +local query = [[ + query user_collection { + user_collection { + user_id + name + stuff { + ... on String_box { + string + } + ... on Int_box { + int + } + ... on List_box { + array + } + ... on Map_box { + map + } + ... on Foo_box { + Foo { + foo1 + foo2 + } + } + } + } + } +]] + +local expected_avro_schema = [[ + type: record + name: Query + fields: + - name: user_collection + type: + type: array + items: + type: record + fields: + - name: user_id + type: string + - name: name + type: string + - name: stuff + type: + - string + - int + - type: map + values: int + - type: record + fields: + - name: foo1 + type: string + - name: foo2 + type: string + name: Foo + namespace: Query.user_collection + - type: array + items: + type: map + values: string + - "null" + name: user_collection + namespace: Query +]] + +expected_avro_schema = yaml.decode(expected_avro_schema) +local gql_query = gql_wrapper:compile(query) + +local avros = gql_query:avro_schema() +test:is_deeply(avros, expected_avro_schema, + "comparision between expected and generated (from query) avro-schemas") + +local result_expected = [[ + user_collection: + - user_id: user_id_1 + name: Nobody + - user_id: user_id_2 + name: Zlata + stuff: + string: Some string + - user_id: user_id_3 + name: Ivan + stuff: + int: 123 + - user_id: user_id_4 + name: Jane + stuff: + map: {'salary': 333, 'deposit': 444} + - user_id: user_id_5 + name: Dan + stuff: + Foo: + foo1: foo1 string + foo2: foo2 string + - user_id: user_id_6 + name: Max + stuff: + array: + - {'salary': 'salary string', 'deposit': 'deposit string'} + - {'salary': 'string salary', 'deposit': 'string deposit'} +]] + +result_expected = yaml.decode(result_expected) +local result = gql_query:execute({}) + +test:is_deeply(result.data, result_expected, + 'comparision between expected and actual query response') + +local ok, compiled_avro, err +ok, compiled_avro = avro.create(avros) +assert(ok) + +ok, err = avro.validate(compiled_avro, result.data) +assert(ok, err) + +test:is(ok, true, 'query response validation by avro') + +testdata.drop_spaces() + +assert(test:check(), 'check plan') + +os.exit() From 286cb8d3b6057f986909cb055650857b413ece6c Mon Sep 17 00:00:00 2001 From: SudoBobo Date: Mon, 6 Aug 2018 13:34:52 +0300 Subject: [PATCH 7/7] Add creation of avro-schema from query with directives Close #85 --- graphql/query_to_avro.lua | 22 + test/extra/to_avro_directives.test.lua | 537 +++++++++++++++++++++++++ 2 files changed, 559 insertions(+) create mode 100755 test/extra/to_avro_directives.test.lua diff --git a/graphql/query_to_avro.lua b/graphql/query_to_avro.lua index e4b4958..dfa1da1 100644 --- a/graphql/query_to_avro.lua +++ b/graphql/query_to_avro.lua @@ -200,6 +200,28 @@ local function field_to_avro(object_type, fields, context) local fieldTypeAvro = gql_type_to_avro(fieldType.kind, subSelections, context) + -- Currently we support only 'include' and 'skip' directives. Both of them + -- affect resulting avro-schema the same way: field with directive becomes + -- nullable, if it's already not. Nullable field does not change. + -- + -- If it is a 1:N connection then it's 'array' field becomes 'array*'. + -- If it is avro-schema union, then 'null' will be added to the union + -- types. If there are more then one directive on a field then all works + -- the same way, like it is only one directive. (But we still check all + -- directives to be 'include' or 'skip'). + if firstField.directives ~= nil then + for _, d in ipairs(firstField.directives) do + check(d.name, "directive.name", "table") + check(d.arguments, "directive.arguments", "table") + check(d.kind, "directive.kind", "string") + assert(d.kind == "directive") + check(d.name.value, "directive.name.value", "string") + assert(d.name.value == "include" or d.name.value == "skip", + "Only 'include' and 'skip' directives are supported for now") + end + fieldTypeAvro = avro_helpers.make_avro_type_nullable(fieldTypeAvro) + end + return { name = convert_schema_helpers.base_name(fieldName), type = fieldTypeAvro, diff --git a/test/extra/to_avro_directives.test.lua b/test/extra/to_avro_directives.test.lua new file mode 100755 index 0000000..ad61efe --- /dev/null +++ b/test/extra/to_avro_directives.test.lua @@ -0,0 +1,537 @@ +#!/usr/bin/env tarantool +local fio = require('fio') +local yaml = require('yaml') +local avro = require('avro_schema') +local test = require('tap').test('to avro schema') + +-- 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 common_testdata = require('test.testdata.common_testdata') +local union_testdata = require('test.testdata.union_testdata') +local multihead_testdata = require('test.testdata.multihead_conn_testdata') +local graphql = require('graphql') + +test:plan(15) + +box.cfg{} + +-- Common testdata + +common_testdata.init_spaces() +local common_meta = common_testdata.get_test_metadata() +common_testdata.fill_test_data(box.space, common_meta) + +local gql_wrapper = graphql.new({ + schemas = common_meta.schemas, + collections = common_meta.collections, + service_fields = common_meta.service_fields, + indexes = common_meta.indexes, + accessor = 'space' +}) + +local common_query = [[ + query order_by_id($order_id: String, $include_description: Boolean, + $skip_discount: Boolean, $include_user: Boolean, + $include_user_first_name: Boolean, $user_id: String, + $include_order_meta: Boolean) { + order_collection(order_id: $order_id) { + order_id + description @include(if: $include_description) + discount @skip(if: $skip_discount) + user_connection(user_id: "user_id_1") @include(if: $include_user) { + user_id + first_name @include(if: $include_user_first_name) + } + order_metainfo_connection(order_id: $order_id) + @skip(if: $include_order_meta) { + order_metainfo_id + } + } + user_collection(user_id: $user_id) @include(if: $include_user) { + user_id + first_name + } + } +]] + +local expected_avro_schema = yaml.decode([[ + type: record + name: Query + fields: + - name: order_collection + type: + type: array + items: + type: record + fields: + - name: order_id + type: string + - name: description + type: string* + - name: discount + type: double* + - name: user_connection + type: + type: record* + fields: + - name: user_id + type: string + - name: first_name + type: string* + name: user_collection + namespace: Query.order_collection + - name: order_metainfo_connection + type: + type: record* + fields: + - name: order_metainfo_id + type: string + name: order_metainfo_collection + namespace: Query.order_collection + name: order_collection + namespace: Query + - name: user_collection + type: + type: array* + items: + type: record + fields: + - name: user_id + type: string + - name: first_name + type: string + name: user_collection + namespace: Query +]]) + +local gql_query = gql_wrapper:compile(common_query) + +local avro_from_query = gql_query:avro_schema() +test:is_deeply(avro_from_query, expected_avro_schema, + 'comparision between expected and generated (from query) avro-schemas ' .. + '- common') + +local ok, compiled_schema = avro.create(avro_from_query) +assert(ok, tostring(compiled_schema)) + +local variables_1 = { + order_id = 'order_id_1', + user_id = 'user_id_1', + include_description = true, + skip_discount = false, + include_user = true, + include_user_first_name = false, + include_order_meta = true +} + +local result_1 = gql_query:execute(variables_1) +result_1 = result_1.data +local result_expected_1 = yaml.decode([[ + user_collection: + - user_id: user_id_1 + first_name: Ivan + order_collection: + - order_id: order_id_1 + discount: 0 + description: first order of Ivan + user_connection: + user_id: user_id_1 +]]) + +test:is_deeply(result_1, result_expected_1, + 'comparision between expected and actual query response - common 1') + +local ok, err = avro.validate(compiled_schema, result_1) +assert(ok, tostring(err)) +test:ok(ok, 'query response validation by avro - common 1') + +local variables_2 = { + order_id = 'order_id_1', + user_id = 'user_id_1', + include_description = false, + skip_discount = true, + include_user = true, + include_user_first_name = true, + include_order_meta = false +} + +local result_2 = gql_query:execute(variables_2) +result_2 = result_2.data +local result_expected_2 = yaml.decode([[ + user_collection: + - user_id: user_id_1 + first_name: Ivan + order_collection: + - order_id: order_id_1 + user_connection: + user_id: user_id_1 + first_name: Ivan + order_metainfo_connection: + order_metainfo_id: order_metainfo_id_1 +]]) + +test:is_deeply(result_2, result_expected_2, + 'comparision between expected and actual query response - common 2') + +local ok, err = avro.validate(compiled_schema, result_2) +assert(ok, tostring(err)) +test:ok(ok, 'query response validation by avro - common 2') + +common_testdata.drop_spaces() + +-- Union testdata + +union_testdata.init_spaces() +local union_meta = union_testdata.get_test_metadata() +union_testdata.fill_test_data(box.space, union_meta) + +local gql_wrapper = graphql.new({ + schemas = union_meta.schemas, + collections = union_meta.collections, + service_fields = union_meta.service_fields, + indexes = union_meta.indexes, + accessor = 'space' +}) + +local union_query = [[ + query user_collection ($include_stuff: Boolean) { + user_collection { + user_id + name + stuff @include(if: $include_stuff) { + ... on String_box { + string + } + ... on Int_box { + int + } + ... on List_box { + array + } + ... on Map_box { + map + } + ... on Foo_box { + Foo { + foo1 + foo2 + } + } + } + } + } +]] + +local expected_avro_schema = yaml.decode([[ + type: record + name: Query + fields: + - name: user_collection + type: + type: array + items: + type: record + fields: + - name: user_id + type: string + - name: name + type: string + - name: stuff + type: + - string + - int + - type: map + values: int + - type: record + fields: + - name: foo1 + type: string + - name: foo2 + type: string + name: Foo + namespace: Query.user_collection + - type: array + items: + type: map + values: string + - "null" + name: user_collection + namespace: Query +]]) + + +local gql_query = gql_wrapper:compile(union_query) + +local avro_from_query = gql_query:avro_schema() +test:is_deeply(avro_from_query, expected_avro_schema, + 'comparision between expected and generated (from query) avro-schemas ' .. + '- union') + +local ok, compiled_schema = avro.create(avro_from_query) +assert(ok, tostring(compiled_schema)) + +local variables_1 = { + include_stuff = true +} + +local result_1 = gql_query:execute(variables_1) +result_1 = result_1.data +local result_expected_1 = yaml.decode([[ + user_collection: + - user_id: user_id_1 + name: Nobody + - user_id: user_id_2 + name: Zlata + stuff: + string: Some string + - user_id: user_id_3 + name: Ivan + stuff: + int: 123 + - user_id: user_id_4 + name: Jane + stuff: + map: {'salary': 333, 'deposit': 444} + - user_id: user_id_5 + name: Dan + stuff: + Foo: + foo1: foo1 string + foo2: foo2 string + - user_id: user_id_6 + name: Max + stuff: + array: + - {'salary': 'salary string', 'deposit': 'deposit string'} + - {'salary': 'string salary', 'deposit': 'string deposit'} +]]) + +test:is_deeply(result_1, result_expected_1, + 'comparision between expected and actual query response - union 1') + +local ok, err = avro.validate(compiled_schema, result_1) +assert(ok, tostring(err)) +test:ok(ok, 'query response validation by avro - union 1') + +local variables_2 = { + include_stuff = false +} + +local result_2 = gql_query:execute(variables_2) +result_2 = result_2.data +local result_expected_2 = yaml.decode([[ + user_collection: + - user_id: user_id_1 + name: Nobody + - user_id: user_id_2 + name: Zlata + - user_id: user_id_3 + name: Ivan + - user_id: user_id_4 + name: Jane + - user_id: user_id_5 + name: Dan + - user_id: user_id_6 + name: Max +]]) + +test:is_deeply(result_2, result_expected_2, + 'comparision between expected and actual query response - union 2') + +local ok, err = avro.validate(compiled_schema, result_2) +assert(ok, tostring(err)) +test:ok(ok, 'query response validation by avro - union 2') + +union_testdata.drop_spaces() + +-- Multi-head connection testdata + +multihead_testdata.init_spaces() +local multihead_meta = multihead_testdata.get_test_metadata() +multihead_testdata.fill_test_data(box.space, multihead_meta) + +local gql_wrapper = graphql.new({ + schemas = multihead_meta.schemas, + collections = multihead_meta.collections, + service_fields = multihead_meta.service_fields, + indexes = multihead_meta.indexes, + accessor = 'space' +}) + +local multihead_query = [[ + query obtainHeroes($hero_id: String, $include_connections: Boolean) { + hero_collection(hero_id: $hero_id) { + hero_id + hero_type + banking_type + hero_connection @include(if: $include_connections) { + ... on box_human_collection { + human_collection { + name + } + } + ... on box_starship_collection { + starship_collection { + model + } + } + } + hero_banking_connection @include(if: $include_connections) { + ... on box_array_credit_account_collection { + credit_account_collection { + account_id + hero_id + } + } + ... on box_array_dublon_account_collection { + dublon_account_collection { + account_id + hero_id + } + } + } + } + } +]] + +local expected_avro_schema = yaml.decode([[ + type: record + name: Query + fields: + - name: hero_collection + type: + type: array + items: + type: record + fields: + - name: hero_id + type: string + - name: hero_type + type: string + - name: banking_type + type: string + - name: hero_connection + type: + type: record* + name: hero_connection + fields: + - name: human_collection + type: + type: record* + fields: + - name: name + type: string + name: human_collection + namespace: Query.hero_collection + - name: starship_collection + type: + type: record* + fields: + - name: model + type: string + name: starship_collection + namespace: Query.hero_collection + - name: hero_banking_connection + type: + type: record* + name: hero_banking_connection + fields: + - name: credit_account_collection + type: + type: array* + items: + type: record + fields: + - name: account_id + type: string + - name: hero_id + type: string + name: credit_account_collection + namespace: Query.hero_collection + - name: dublon_account_collection + type: + type: array* + items: + type: record + fields: + - name: account_id + type: string + - name: hero_id + type: string + name: dublon_account_collection + namespace: Query.hero_collection + name: hero_collection + namespace: Query +]]) + +local gql_query = gql_wrapper:compile(multihead_query) + +local avro_from_query = gql_query:avro_schema() + +test:is_deeply(avro_from_query, expected_avro_schema, + 'comparision between expected and generated (from query) avro-schemas ' .. + '- multihead') + +local ok, compiled_schema = avro.create(avro_from_query) +assert(ok, tostring(compiled_schema)) + +local variables_1 = { + hero_id = 'hero_id_1', + include_connections = true +} + +local result_1 = gql_query:execute(variables_1) +result_1 = result_1.data +local result_expected_1 = yaml.decode([[ + hero_collection: + - hero_id: hero_id_1 + hero_type: human + hero_connection: + human_collection: + name: Luke + banking_type: credit + hero_banking_connection: + credit_account_collection: + - account_id: credit_account_id_1 + hero_id: hero_id_1 + - account_id: credit_account_id_2 + hero_id: hero_id_1 + - account_id: credit_account_id_3 + hero_id: hero_id_1 +]]) + +test:is_deeply(result_1, result_expected_1, + 'comparision between expected and actual query response - multihead 1') + +local ok, err = avro.validate(compiled_schema, result_1) +assert(ok, tostring(err)) +test:ok(ok, 'query response validation by avro - multihead 1') + +local variables_2 = { + hero_id = 'hero_id_2', + include_connections = false +} + +local result_2 = gql_query:execute(variables_2) +result_2 = result_2.data +local result_expected_2 = yaml.decode([[ + hero_collection: + - hero_id: hero_id_2 + hero_type: starship + banking_type: dublon +]]) + +test:is_deeply(result_2, result_expected_2, + 'comparision between expected and actual query response - multihead 2') + +local ok, err = avro.validate(compiled_schema, result_2) +assert(ok, tostring(err)) +test:ok(ok, 'query response validation by avro - multihead 2') + +multihead_testdata.drop_spaces() + +assert(test:check(), 'check plan') + +os.exit()