diff --git a/lib/shopify_custom_data_graphql.rb b/lib/shopify_custom_data_graphql.rb index 491d1d1..e32bf92 100644 --- a/lib/shopify_custom_data_graphql.rb +++ b/lib/shopify_custom_data_graphql.rb @@ -24,6 +24,46 @@ def span(span_name) result end end + + class << self + def handle_introspection(query) + return unless query.query? + + introspection_nodes = map_root_introspection_nodes(query, query.schema.query, query.selected_operation.selections) + return if introspection_nodes.all? { _1.nil? || _1.name == "__typename" } + + errors = if introspection_nodes.any?(&:nil?) + introspection_nodes.reject! { _1.nil? || _1.name == "__typename" } + [ + GraphQL::StaticValidation::Error.new( + "Cannot combine root fields with introspection fields.", + nodes: introspection_nodes, + ), + ] + end + + yield(errors) + nil + end + + private + + def map_root_introspection_nodes(query, parent_type, selections, nodes: []) + selections.each do |node| + case node + when GraphQL::Language::Nodes::Field + field = query.get_field(parent_type, node.name) + nodes << (field&.introspection? ? node : nil) + when GraphQL::Language::Nodes::InlineFragment + map_root_introspection_nodes(query, parent_type, node.selections, nodes: nodes) + when GraphQL::Language::Nodes::FragmentSpread + fragment_selections = query.fragments[node.name].selections + map_root_introspection_nodes(query, parent_type, fragment_selections, nodes: nodes) + end + end + nodes + end + end end require_relative "shopify_custom_data_graphql/metafield_type_resolver" diff --git a/lib/shopify_custom_data_graphql/client.rb b/lib/shopify_custom_data_graphql/client.rb index e0796cb..5432754 100644 --- a/lib/shopify_custom_data_graphql/client.rb +++ b/lib/shopify_custom_data_graphql/client.rb @@ -145,16 +145,22 @@ def perform_query(query_str, operation_name, tracer, &block) query = tracer.span("parse") do GraphQL::Query.new(schema, query: query_str, operation_name: operation_name) end + errors = tracer.span("validate") do schema.static_validator.validate(query)[:errors] end - raise ValidationError.new(errors: errors.map(&:to_h)) if errors.any? - if introspection_query?(query) - result = tracer.span("introspection") { query.result.to_h } - return PreparedQuery::Result.new(query: query_str, tracer: tracer, result: result) + ShopifyCustomDataGraphQL.handle_introspection(query) do |introspection_errors| + if introspection_errors + errors.concat(introspection_errors) + else + result = tracer.span("introspection") { query.result.to_h } + return PreparedQuery::Result.new(query: query_str, tracer: tracer, result: result) + end end + raise ValidationError.new(errors: errors.map(&:to_h)) if errors.any? + prepared_query = tracer.span("transform_request") do RequestTransformer.new(query).perform end @@ -164,34 +170,6 @@ def perform_query(query_str, operation_name, tracer, &block) prepared_query.perform(tracer, &block) end - def introspection_query?(query) - return false unless query.query? - - root_field_names = collect_root_field_names(query, query.selected_operation.selections) - return false if root_field_names.none? { INTROSPECTION_FIELDS.include?(_1) } - - # hard limitation... data and introspections resolve from different places - unless root_field_names.all? { INTROSPECTION_FIELDS.include?(_1) } - raise ValidationError, "Custom data schemas cannot combine data fields with introspection fields." - end - - true - end - - def collect_root_field_names(query, selections, names = []) - selections.each do |node| - case node - when GraphQL::Language::Nodes::Field - names << node.name - when GraphQL::Language::Nodes::InlineFragment - collect_root_field_names(query, node.selections, names) - when GraphQL::Language::Nodes::FragmentSpread - collect_root_field_names(query, query.fragments[node.name].selections, names) - end - end - names - end - def schema_file_name(handle, app_context_id = nil) file_name = "#{handle}_#{@admin.api_version}_cli#{@admin.api_client_id}" file_name << "_app#{app_context_id}" if app_context_id diff --git a/test/shopify_custom_data_graphql/handle_introspection_test.rb b/test/shopify_custom_data_graphql/handle_introspection_test.rb new file mode 100644 index 0000000..47ea382 --- /dev/null +++ b/test/shopify_custom_data_graphql/handle_introspection_test.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "test_helper" + +describe "handle_introspection" do + def test_no_action_for_only_root_fields + called = false + query = GraphQL::Query.new(shop_schema, %|{ product }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do + called = true + end + + assert_equal false, called + end + + def test_no_action_for_root_fields_with_typename + called = false + query = GraphQL::Query.new(shop_schema, %|{ product __typename }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do + called = true + end + + assert_equal false, called + end + + def test_action_with_no_errors_for_only_introspection + called = false + query = GraphQL::Query.new(shop_schema, %|{ __schema }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do |errors| + called = true + assert errors.nil? + end + + assert called, "expected to be called" + end + + def test_action_with_no_errors_for_introspection_and_typename + called = false + query = GraphQL::Query.new(shop_schema, %|{ __schema __typename }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do |errors| + called = true + assert errors.nil? + end + + assert called, "expected to be called" + end + + def test_action_with_errors_for_introspection_and_root_field + called = false + query = GraphQL::Query.new(shop_schema, %|{ product __schema __typename }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do |errors| + called = true + assert errors + assert_equal 1, errors.first.nodes.length + assert_equal "Cannot combine root fields with introspection fields.", errors.first.to_h["message"] + end + + assert called, "expected to be called" + end + + def test_action_with_errors_for_introspection_and_root_field_across_inline_fragment + called = false + query = GraphQL::Query.new(shop_schema, %|{ product ...on QueryRoot { __schema } }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do |errors| + called = true + assert_equal 1, errors.length + end + + assert called, "expected to be called" + end + + def test_action_with_errors_for_introspection_and_root_field_across_fragment_spread + called = false + query = GraphQL::Query.new(shop_schema, %|{ product ...Boom } fragment Boom on QueryRoot { __schema }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do |errors| + called = true + assert_equal 1, errors.length + end + + assert called, "expected to be called" + end + + def test_gracefully_ignores_invalid_field_names + called = false + query = GraphQL::Query.new(shop_schema, %|{ sfoo }|) + ShopifyCustomDataGraphQL.handle_introspection(query) do + called = true + end + + assert_equal false, called + end +end