Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions lib/shopify_custom_data_graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
42 changes: 10 additions & 32 deletions lib/shopify_custom_data_graphql/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
92 changes: 92 additions & 0 deletions test/shopify_custom_data_graphql/handle_introspection_test.rb
Original file line number Diff line number Diff line change
@@ -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