diff --git a/lib/graphql/client.rb b/lib/graphql/client.rb index 83bad2d5..7741c080 100644 --- a/lib/graphql/client.rb +++ b/lib/graphql/client.rb @@ -2,6 +2,7 @@ require "active_support/inflector" require "active_support/notifications" require "graphql" +require "graphql/client/type_stack" require "graphql/client/collocated_enforcement" require "graphql/client/definition_variables" require "graphql/client/definition" @@ -425,12 +426,26 @@ def sliced_definitions(document_dependencies, doc, source_location:) end.to_h end + class GatherNamesVisitor < GraphQL::Language::Visitor + def initialize(node) + @names = [] + super + end + + attr_reader :names + + def on_fragment_spread(node, parent) + @names << node.name + super + end + end + def find_definition_dependencies(node) - names = [] - visitor = Language::Visitor.new(node) - visitor[Language::Nodes::FragmentSpread] << -> (node, parent) { names << node.name } + visitor = GatherNamesVisitor.new(node) visitor.visit - names.uniq + names = visitor.names + names.uniq! + names end def deep_freeze_json_object(obj) diff --git a/lib/graphql/client/definition.rb b/lib/graphql/client/definition.rb index 8df0745b..f1ff628c 100644 --- a/lib/graphql/client/definition.rb +++ b/lib/graphql/client/definition.rb @@ -147,41 +147,58 @@ def new(obj, errors = Errors.new) # Internal: Nodes AST indexes. def indexes @indexes ||= begin - visitor = GraphQL::Language::Visitor.new(document) - definitions = index_node_definitions(visitor) - spreads = index_spreads(visitor) + visitor = DefinitionVisitor.new(document) visitor.visit - { definitions: definitions, spreads: spreads } + { definitions: visitor.definitions, spreads: visitor.spreads } end end - private + class DefinitionVisitor < GraphQL::Language::Visitor + attr_reader :spreads, :definitions - def cast_object(obj) - if obj.class.is_a?(GraphQL::Client::Schema::ObjectType) - unless obj._spreads.include?(definition_node.name) - raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}" - end - schema_class.cast(obj.to_h, obj.errors) - else - raise TypeError, "unexpected #{obj.class}" - end + def initialize(doc) + super + @spreads = {} + @definitions = {} + @current_definition = nil end - EMPTY_SET = Set.new.freeze + def on_field(node, parent) + @definitions[node] = @current_definition + @spreads[node] = get_spreads(node) + super + end - def index_spreads(visitor) - spreads = {} - on_node = ->(node, _parent) do - node_spreads = flatten_spreads(node).map(&:name) - spreads[node] = node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze - end + def on_fragment_definition(node, parent) + @current_definition = node + @definitions[node] = @current_definition + @spreads[node] = get_spreads(node) + super + ensure + @current_definition = nil + end - visitor[GraphQL::Language::Nodes::Field] << on_node - visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node - visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node + def on_operation_definition(node, parent) + @current_definition = node + @definitions[node] = @current_definition + @spreads[node] = get_spreads(node) + super + ensure + @current_definition = nil + end - spreads + def on_inline_fragment(node, parent) + @definitions[node] = @current_definition + super + end + + private + + EMPTY_SET = Set.new.freeze + + def get_spreads(node) + node_spreads = flatten_spreads(node).map(&:name) + node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze end def flatten_spreads(node) @@ -198,24 +215,19 @@ def flatten_spreads(node) end spreads end + end + + private - def index_node_definitions(visitor) - current_definition = nil - enter_definition = ->(node, _parent) { current_definition = node } - leave_definition = ->(node, _parent) { current_definition = nil } - - visitor[GraphQL::Language::Nodes::FragmentDefinition].enter << enter_definition - visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << leave_definition - visitor[GraphQL::Language::Nodes::OperationDefinition].enter << enter_definition - visitor[GraphQL::Language::Nodes::OperationDefinition].leave << leave_definition - - definitions = {} - on_node = ->(node, _parent) { definitions[node] = current_definition } - visitor[GraphQL::Language::Nodes::Field] << on_node - visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node - visitor[GraphQL::Language::Nodes::InlineFragment] << on_node - visitor[GraphQL::Language::Nodes::OperationDefinition] << on_node - definitions + def cast_object(obj) + if obj.class.is_a?(GraphQL::Client::Schema::ObjectType) + unless obj._spreads.include?(definition_node.name) + raise TypeError, "#{definition_node.name} is not included in #{obj.source_definition.name}" + end + schema_class.cast(obj.to_h, obj.errors) + else + raise TypeError, "unexpected #{obj.class}" + end end end end diff --git a/lib/graphql/client/definition_variables.rb b/lib/graphql/client/definition_variables.rb index a85c859c..7520f9cc 100644 --- a/lib/graphql/client/definition_variables.rb +++ b/lib/graphql/client/definition_variables.rb @@ -24,26 +24,33 @@ def self.variables(schema, document, definition_name = nil) sliced_document = GraphQL::Language::DefinitionSlice.slice(document, definition_name) - visitor = GraphQL::Language::Visitor.new(sliced_document) - type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor) + visitor = VariablesVisitor.new(sliced_document, schema: schema) + visitor.visit + visitor.variables + end + + class VariablesVisitor < GraphQL::Language::Visitor + prepend GraphQL::Client::TypeStack + + def initialize(*_args, **_kwargs) + super + @variables = {} + end - variables = {} + attr_reader :variables - visitor[GraphQL::Language::Nodes::VariableIdentifier] << ->(node, parent) do - if definition = type_stack.argument_definitions.last - existing_type = variables[node.name.to_sym] + def on_variable_identifier(node, parent) + if definition = @argument_definitions.last + existing_type = @variables[node.name.to_sym] if existing_type && existing_type.unwrap != definition.type.unwrap raise GraphQL::Client::ValidationError, "$#{node.name} was already declared as #{existing_type.unwrap}, but was #{definition.type.unwrap}" elsif !(existing_type && existing_type.kind.non_null?) - variables[node.name.to_sym] = definition.type + @variables[node.name.to_sym] = definition.type end end + super end - - visitor.visit - - variables end # Internal: Detect all variables used in a given operation or fragment diff --git a/lib/graphql/client/document_types.rb b/lib/graphql/client/document_types.rb index d65a2805..4921466c 100644 --- a/lib/graphql/client/document_types.rb +++ b/lib/graphql/client/document_types.rb @@ -1,10 +1,41 @@ # frozen_string_literal: true require "graphql" +require "graphql/client/type_stack" module GraphQL class Client # Internal: Use schema to detect definition and field types. module DocumentTypes + class AnalyzeTypesVisitor < GraphQL::Language::Visitor + prepend GraphQL::Client::TypeStack + attr_reader :fields + + def initialize(*a, **kw) + @fields = {} + super + end + + def on_operation_definition(node, _parent) + @fields[node] = @object_types.last + super + end + + def on_fragment_definition(node, _parent) + @fields[node] = @object_types.last + super + end + + def on_inline_fragment(node, _parent) + @fields[node] = @object_types.last + super + end + + def on_field(node, _parent) + @fields[node] = @field_definitions.last.type + super + end + end + # Internal: Detect all types used in a given document # # schema - A GraphQL::Schema @@ -20,32 +51,15 @@ def self.analyze_types(schema, document) raise TypeError, "expected schema to be a GraphQL::Language::Nodes::Document, but was #{document.class}" end - visitor = GraphQL::Language::Visitor.new(document) - type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor) - - fields = {} - - visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, _parent) do - fields[node] = type_stack.object_types.last - end - visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, _parent) do - fields[node] = type_stack.object_types.last - end - visitor[GraphQL::Language::Nodes::InlineFragment] << ->(node, _parent) do - fields[node] = type_stack.object_types.last - end - visitor[GraphQL::Language::Nodes::Field] << ->(node, _parent) do - fields[node] = type_stack.field_definitions.last.type - end + visitor = AnalyzeTypesVisitor.new(document, schema: schema) visitor.visit - - fields + visitor.fields rescue StandardError => err if err.is_a?(TypeError) raise end # FIXME: TypeStack my crash on invalid documents - fields + visitor.fields end end end diff --git a/lib/graphql/client/type_stack.rb b/lib/graphql/client/type_stack.rb new file mode 100644 index 00000000..678815ad --- /dev/null +++ b/lib/graphql/client/type_stack.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true +module GraphQL + class Client + module TypeStack + # @return [GraphQL::Schema] the schema whose types are present in this document + attr_reader :schema + + # When it enters an object (starting with query or mutation root), it's pushed on this stack. + # When it exits, it's popped off. + # @return [Array] + attr_reader :object_types + + # When it enters a field, it's pushed on this stack (useful for nested fields, args). + # When it exits, it's popped off. + # @return [Array] fields which have been entered + attr_reader :field_definitions + + # Directives are pushed on, then popped off while traversing the tree + # @return [Array] directives which have been entered + attr_reader :directive_definitions + + # @return [Array] arguments which have been entered + attr_reader :argument_definitions + + # @return [Array] fields which have been entered (by their AST name) + attr_reader :path + + # @param schema [GraphQL::Schema] the schema whose types to use when climbing this document + # @param visitor [GraphQL::Language::Visitor] a visitor to follow & watch the types + def initialize(document, schema:, **rest) + @schema = schema + @object_types = [] + @field_definitions = [] + @directive_definitions = [] + @argument_definitions = [] + @path = [] + super(document, **rest) + end + + def on_directive(node, parent) + directive_defn = @schema.directives[node.name] + @directive_definitions.push(directive_defn) + super(node, parent) + ensure + @directive_definitions.pop + end + + def on_field(node, parent) + parent_type = @object_types.last + parent_type = parent_type.unwrap + + field_definition = @schema.get_field(parent_type, node.name) + @field_definitions.push(field_definition) + if !field_definition.nil? + next_object_type = field_definition.type + @object_types.push(next_object_type) + else + @object_types.push(nil) + end + @path.push(node.alias || node.name) + super(node, parent) + ensure + @field_definitions.pop + @object_types.pop + @path.pop + end + + def on_argument(node, parent) + if @argument_definitions.last + arg_type = @argument_definitions.last.type.unwrap + if arg_type.kind.input_object? + argument_defn = arg_type.arguments[node.name] + else + argument_defn = nil + end + elsif @directive_definitions.last + argument_defn = @directive_definitions.last.arguments[node.name] + elsif @field_definitions.last + argument_defn = @field_definitions.last.arguments[node.name] + else + argument_defn = nil + end + @argument_definitions.push(argument_defn) + @path.push(node.name) + super(node, parent) + ensure + @argument_definitions.pop + @path.pop + end + + def on_operation_definition(node, parent) + # eg, QueryType, MutationType + object_type = @schema.root_type_for_operation(node.operation_type) + @object_types.push(object_type) + @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}") + super(node, parent) + ensure + @object_types.pop + @path.pop + end + + def on_inline_fragment(node, parent) + object_type = if node.type + @schema.get_type(node.type.name) + else + @object_types.last + end + if !object_type.nil? + object_type = object_type.unwrap + end + @object_types.push(object_type) + @path.push("...#{node.type ? " on #{node.type.to_query_string}" : ""}") + super(node, parent) + ensure + @object_types.pop + @path.pop + end + + def on_fragment_definition(node, parent) + object_type = if node.type + @schema.get_type(node.type.name) + else + @object_types.last + end + if !object_type.nil? + object_type = object_type.unwrap + end + @object_types.push(object_type) + @path.push("fragment #{node.name}") + super(node, parent) + ensure + @object_types.pop + @path.pop + end + + def on_fragment_spread(node, parent) + @path.push("... #{node.name}") + super(node, parent) + ensure + @path.pop + end + end + end +end diff --git a/lib/rubocop/cop/graphql/overfetch.rb b/lib/rubocop/cop/graphql/overfetch.rb index 15469aad..bd98084d 100644 --- a/lib/rubocop/cop/graphql/overfetch.rb +++ b/lib/rubocop/cop/graphql/overfetch.rb @@ -21,19 +21,12 @@ def investigate(processed_source) query, = ::GraphQL::Client::ViewModule.extract_graphql_section(erb) return unless query - aliases = {} - fields = {} - ranges = {} - # TODO: Use GraphQL client parser document = ::GraphQL.parse(query.gsub(/::/, "__")) - - visitor = ::GraphQL::Language::Visitor.new(document) - visitor[::GraphQL::Language::Nodes::Field] << ->(node, _parent) do - name = node.alias || node.name - fields[name] ||= 0 - field_aliases(name).each { |n| (aliases[n] ||= []) << name } - ranges[name] ||= source_range(processed_source.buffer, node.line, 0) + visitor = OverfetchVisitor.new(document) do |line_num| + # `source_range` is private to this object, + # so yield back out to it to get this info: + source_range(processed_source.buffer, line_num, 0) end visitor.visit @@ -41,30 +34,53 @@ def investigate(processed_source) method_names = method_names_for(*node) method_names.each do |method_name| - aliases.fetch(method_name, []).each do |field_name| - fields[field_name] += 1 + visitor.aliases.fetch(method_name, []).each do |field_name| + visitor.fields[field_name] += 1 end end end - fields.each do |field, count| + visitor.fields.each do |field, count| next if count > 0 - add_offense(nil, location: ranges[field], message: "GraphQL field '#{field}' query but was not used in template.") + add_offense(nil, location: visitor.ranges[field], message: "GraphQL field '#{field}' query but was not used in template.") end end - def field_aliases(name) - names = Set.new + class OverfetchVisitor < ::GraphQL::Language::Visitor + def initialize(doc, &range_for_line) + super(doc) + @range_for_line = range_for_line + @fields = {} + @aliases = {} + @ranges = {} + end + + attr_reader :fields, :aliases, :ranges + + def on_field(node, parent) + name = node.alias || node.name + fields[name] ||= 0 + field_aliases(name).each { |n| (aliases[n] ||= []) << name } + ranges[name] ||= @range_for_line.call(node.line) + super + end - names << name - names << "#{name}?" + private - names << underscore_name = ActiveSupport::Inflector.underscore(name) - names << "#{underscore_name}?" + def field_aliases(name) + names = Set.new - names + names << name + names << "#{name}?" + + names << underscore_name = ActiveSupport::Inflector.underscore(name) + names << "#{underscore_name}?" + + names + end end + def method_names_for(*node) receiver, method_name, *_args = node method_names = [] diff --git a/test/test_client.rb b/test/test_client.rb index e2956302..da38ff70 100644 --- a/test/test_client.rb +++ b/test/test_client.rb @@ -4,9 +4,7 @@ require "json" require "minitest/autorun" -class TestClient < MiniTest::Test - GraphQL::DeprecatedDSL.activate if GraphQL::VERSION > "1.8" - +class TestClient < Minitest::Test module NodeType include GraphQL::Schema::Interface field :id, ID, null: false @@ -79,17 +77,13 @@ class MutationType < GraphQL::Schema::Object class Schema < GraphQL::Schema query(QueryType) mutation(MutationType) - if defined?(GraphQL::Execution::Interpreter) - use GraphQL::Execution::Interpreter - use GraphQL::Analysis::AST - end end module Temp end def setup - @client = GraphQL::Client.new(schema: Schema.graphql_definition) + @client = GraphQL::Client.new(schema: Schema) @client.document_tracking_enabled = true end diff --git a/test/test_client_create_operation.rb b/test/test_client_create_operation.rb index 6d71423d..5eab3431 100644 --- a/test/test_client_create_operation.rb +++ b/test/test_client_create_operation.rb @@ -3,7 +3,7 @@ require "graphql/client" require "minitest/autorun" -class TestClientCreateOperation < MiniTest::Test +class TestClientCreateOperation < Minitest::Test class UserType < GraphQL::Schema::Object field :id, ID, null: false end @@ -32,11 +32,6 @@ class MutationType < GraphQL::Schema::Object class Schema < GraphQL::Schema query(QueryType) mutation(MutationType) - - if defined?(GraphQL::Execution::Interpreter) - use GraphQL::Execution::Interpreter - use GraphQL::Analysis::AST - end end module Temp diff --git a/test/test_client_errors.rb b/test/test_client_errors.rb index 6694df2e..84bfd0f4 100644 --- a/test/test_client_errors.rb +++ b/test/test_client_errors.rb @@ -3,7 +3,7 @@ require "graphql/client" require "minitest/autorun" -class TestClientErrors < MiniTest::Test +class TestClientErrors < Minitest::Test class FooType < GraphQL::Schema::Object field :nullable_error, String, null: true def nullable_error @@ -55,11 +55,6 @@ def foos class Schema < GraphQL::Schema query(QueryType) - - if defined?(GraphQL::Execution::Interpreter) - use GraphQL::Execution::Interpreter - use GraphQL::Analysis::AST - end end module Temp diff --git a/test/test_client_fetch.rb b/test/test_client_fetch.rb index 1991f490..e438f701 100644 --- a/test/test_client_fetch.rb +++ b/test/test_client_fetch.rb @@ -3,7 +3,7 @@ require "graphql/client" require "minitest/autorun" -class TestClientFetch < MiniTest::Test +class TestClientFetch < Minitest::Test class QueryType < GraphQL::Schema::Object field :version, Integer, null: false def version diff --git a/test/test_client_schema.rb b/test/test_client_schema.rb index b9bb15ad..5c626237 100644 --- a/test/test_client_schema.rb +++ b/test/test_client_schema.rb @@ -4,7 +4,7 @@ require "json" require "minitest/autorun" -class TestClientSchema < MiniTest::Test +class TestClientSchema < Minitest::Test FakeConn = Class.new do attr_reader :context diff --git a/test/test_client_validation.rb b/test/test_client_validation.rb index a95b4f40..c83aa5f4 100644 --- a/test/test_client_validation.rb +++ b/test/test_client_validation.rb @@ -3,7 +3,7 @@ require "graphql/client" require "minitest/autorun" -class TestClientValidation < MiniTest::Test +class TestClientValidation < Minitest::Test class UserType < GraphQL::Schema::Object field :name, String, null: false end diff --git a/test/test_collocated_enforcement.rb b/test/test_collocated_enforcement.rb index 08d45b6d..7f5bec97 100644 --- a/test/test_collocated_enforcement.rb +++ b/test/test_collocated_enforcement.rb @@ -3,7 +3,7 @@ require "minitest/autorun" require_relative "foo_helper" -class TestCollocatedEnforcement < MiniTest::Test +class TestCollocatedEnforcement < Minitest::Test include FooHelper class Person diff --git a/test/test_definition_variables.rb b/test/test_definition_variables.rb index 2a7ac757..af63747d 100644 --- a/test/test_definition_variables.rb +++ b/test/test_definition_variables.rb @@ -4,7 +4,7 @@ require "graphql/client/definition_variables" require "minitest/autorun" -class TestDefinitionVariables < MiniTest::Test +class TestDefinitionVariables < Minitest::Test class QueryType < GraphQL::Schema::Object field :version, Integer, null: false field :user, String, null: false do diff --git a/test/test_erb.rb b/test/test_erb.rb index 75830dcb..765b38ff 100644 --- a/test/test_erb.rb +++ b/test/test_erb.rb @@ -8,7 +8,7 @@ require "graphql/client/view_module" require "minitest/autorun" -class TestERB < MiniTest::Test +class TestERB < Minitest::Test class ErubiEngine < Erubi::Engine include GraphQL::Client::ErubiEnhancer end diff --git a/test/test_hash_with_indifferent_access.rb b/test/test_hash_with_indifferent_access.rb index f2797604..0ae2f309 100644 --- a/test/test_hash_with_indifferent_access.rb +++ b/test/test_hash_with_indifferent_access.rb @@ -2,7 +2,7 @@ require "graphql/client/hash_with_indifferent_access" require "minitest/autorun" -class TestHashWithIndifferentAccess < MiniTest::Test +class TestHashWithIndifferentAccess < Minitest::Test def test_string_access hash = GraphQL::Client::HashWithIndifferentAccess.new("foo" => 42) assert_equal 42, hash["foo"] diff --git a/test/test_http.rb b/test/test_http.rb index 3f0418d8..4c7deb68 100644 --- a/test/test_http.rb +++ b/test/test_http.rb @@ -3,7 +3,7 @@ require "graphql/client/http" require "minitest/autorun" -class TestHTTP < MiniTest::Test +class TestHTTP < Minitest::Test SWAPI = GraphQL::Client::HTTP.new("https://mpjk0plp9.lp.gql.zone/graphql") do def headers(_context) { "User-Agent" => "GraphQL/1.0" } diff --git a/test/test_object_typename.rb b/test/test_object_typename.rb index 6b94b504..3b7bf735 100644 --- a/test/test_object_typename.rb +++ b/test/test_object_typename.rb @@ -4,7 +4,7 @@ require "minitest/autorun" require "ostruct" -class TestObjectTypename < MiniTest::Test +class TestObjectTypename < Minitest::Test class PersonType < GraphQL::Schema::Object field :id, Integer, null: true def id; 42; end diff --git a/test/test_operation_slice.rb b/test/test_operation_slice.rb index 9ab24815..b6caa8a4 100644 --- a/test/test_operation_slice.rb +++ b/test/test_operation_slice.rb @@ -2,7 +2,7 @@ require "graphql" require "minitest/autorun" -class TestDefinitionSlice < MiniTest::Test +class TestDefinitionSlice < Minitest::Test def test_slice_simple_query_operation document = GraphQL.parse(<<-'GRAPHQL') query FooQuery { diff --git a/test/test_query_result.rb b/test/test_query_result.rb index b0688dd6..e0e725ef 100644 --- a/test/test_query_result.rb +++ b/test/test_query_result.rb @@ -6,7 +6,7 @@ require "ostruct" require_relative "foo_helper" -class TestQueryResult < MiniTest::Test +class TestQueryResult < Minitest::Test class DateTimeType < GraphQL::Schema::Scalar def self.coerce_input(value, ctx) Time.iso8601(value) diff --git a/test/test_query_typename.rb b/test/test_query_typename.rb index 63bf2326..b8df0b9d 100644 --- a/test/test_query_typename.rb +++ b/test/test_query_typename.rb @@ -3,7 +3,7 @@ require "graphql/client/query_typename" require "minitest/autorun" -class TestQueryTypename < MiniTest::Test +class TestQueryTypename < Minitest::Test class PersonType < GraphQL::Schema::Object field :id, Integer, null: true def id; 42; end diff --git a/test/test_rubocop_heredoc.rb b/test/test_rubocop_heredoc.rb index 5e481080..0010a208 100644 --- a/test/test_rubocop_heredoc.rb +++ b/test/test_rubocop_heredoc.rb @@ -2,7 +2,7 @@ require "rubocop/cop/graphql/heredoc" require "minitest/autorun" -class TestRubocopHeredoc < MiniTest::Test +class TestRubocopHeredoc < Minitest::Test def setup config = RuboCop::Config.new @cop = RuboCop::Cop::GraphQL::Heredoc.new(config) diff --git a/test/test_rubocop_overfetch.rb b/test/test_rubocop_overfetch.rb index cf25a39a..13319aa8 100644 --- a/test/test_rubocop_overfetch.rb +++ b/test/test_rubocop_overfetch.rb @@ -3,7 +3,7 @@ require "rubocop/cop/graphql/overfetch" require "minitest/autorun" -class TestRubocopOverfetch < MiniTest::Test +class TestRubocopOverfetch < Minitest::Test Root = File.expand_path("..", __FILE__) def setup diff --git a/test/test_schema.rb b/test/test_schema.rb index 58aca8ee..32aa0409 100644 --- a/test/test_schema.rb +++ b/test/test_schema.rb @@ -4,7 +4,7 @@ require "minitest/autorun" require "time" -class TestSchemaType < MiniTest::Test +class TestSchemaType < Minitest::Test GraphQL::DeprecatedDSL.activate if GraphQL::VERSION > "1.8" DateTime = GraphQL::ScalarType.define do diff --git a/test/test_view_module.rb b/test/test_view_module.rb index 35f362f4..e9c87ab8 100644 --- a/test/test_view_module.rb +++ b/test/test_view_module.rb @@ -4,7 +4,7 @@ require "graphql/client/view_module" require "minitest/autorun" -class TestViewModule < MiniTest::Test +class TestViewModule < Minitest::Test Root = File.expand_path("..", __FILE__) class UserType < GraphQL::Schema::Object