diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index badf26e209..5ee2dbf93a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: - gemfile: gemfiles/rails_7.0.gemfile ruby: 3.1 - gemfile: gemfiles/rails_master.gemfile - ruby: 3.1 + ruby: 3.2 - gemfile: gemfiles/rails_7.0.gemfile ruby: 3.2 - gemfile: gemfiles/rails_7.1.gemfile diff --git a/lib/graphql/analysis/field_usage.rb b/lib/graphql/analysis/field_usage.rb index 3b8a51a1d9..a54767dd78 100644 --- a/lib/graphql/analysis/field_usage.rb +++ b/lib/graphql/analysis/field_usage.rb @@ -72,7 +72,7 @@ def extract_deprecated_arguments(argument_values) end def extract_deprecated_enum_value(enum_type, value) - enum_value = @query.warden.enum_values(enum_type).find { |ev| ev.value == value } + enum_value = @query.types.enum_values(enum_type).find { |ev| ev.value == value } if enum_value&.deprecation_reason @used_deprecated_enum_values << enum_value.path end diff --git a/lib/graphql/analysis/query_complexity.rb b/lib/graphql/analysis/query_complexity.rb index 96a2579a23..6d59b3dd5d 100644 --- a/lib/graphql/analysis/query_complexity.rb +++ b/lib/graphql/analysis/query_complexity.rb @@ -98,7 +98,7 @@ def merged_max_complexity_for_scopes(query, scopes) possible_scope_types.keys.each do |possible_scope_type| next unless possible_scope_type.kind.abstract? - query.possible_types(possible_scope_type).each do |impl_type| + query.types.possible_types(possible_scope_type).each do |impl_type| possible_scope_types[impl_type] ||= true end possible_scope_types.delete(possible_scope_type) @@ -123,8 +123,8 @@ def merged_max_complexity_for_scopes(query, scopes) def types_intersect?(query, a, b) return true if a == b - a_types = query.possible_types(a) - query.possible_types(b).any? { |t| a_types.include?(t) } + a_types = query.types.possible_types(a) + query.types.possible_types(b).any? { |t| a_types.include?(t) } end # A hook which is called whenever a field's max complexity is calculated. diff --git a/lib/graphql/analysis/visitor.rb b/lib/graphql/analysis/visitor.rb index 5d48cf38fc..5ee8422450 100644 --- a/lib/graphql/analysis/visitor.rb +++ b/lib/graphql/analysis/visitor.rb @@ -21,6 +21,7 @@ def initialize(query:, analyzers:) @rescued_errors = [] @query = query @schema = query.schema + @types = query.types @response_path = [] @skip_stack = [false] super(query.selected_operation) @@ -131,7 +132,7 @@ def on_field(node, parent) @response_path.push(node.alias || node.name) parent_type = @object_types.last # This could be nil if the previous field wasn't found: - field_definition = parent_type && @schema.get_field(parent_type, node.name, @query.context) + field_definition = parent_type && @types.field(parent_type, node.name) @field_definitions.push(field_definition) if !field_definition.nil? next_object_type = field_definition.type.unwrap @@ -167,14 +168,14 @@ def on_argument(node, parent) argument_defn = if (arg = @argument_definitions.last) arg_type = arg.type.unwrap if arg_type.kind.input_object? - arg_type.get_argument(node.name, @query.context) + @types.argument(arg_type, node.name) else nil end elsif (directive_defn = @directive_definitions.last) - directive_defn.get_argument(node.name, @query.context) + @types.argument(directive_defn, node.name) elsif (field_defn = @field_definitions.last) - field_defn.get_argument(node.name, @query.context) + @types.argument(field_defn, node.name) else nil end @@ -245,7 +246,7 @@ def enter_fragment_spread_inline(fragment_spread) fragment_def = query.fragments[fragment_spread.name] object_type = if fragment_def.type - @query.warden.get_type(fragment_def.type.name) + @types.type(fragment_def.type.name) else object_types.last end @@ -268,7 +269,7 @@ def skip?(ast_node) def on_fragment_with_type(node) object_type = if node.type - @query.warden.get_type(node.type.name) + @types.type(node.type.name) else @object_types.last end diff --git a/lib/graphql/execution/interpreter/runtime.rb b/lib/graphql/execution/interpreter/runtime.rb index 02182060c3..87bbcc3d9d 100644 --- a/lib/graphql/execution/interpreter/runtime.rb +++ b/lib/graphql/execution/interpreter/runtime.rb @@ -160,9 +160,9 @@ def gather_selections(owner_object, owner_type, selections, selections_to_run = case node when GraphQL::Language::Nodes::InlineFragment if node.type - type_defn = schema.get_type(node.type.name, context) + type_defn = query.types.type(node.type.name) - if query.warden.possible_types(type_defn).include?(owner_type) + if query.types.possible_types(type_defn).include?(owner_type) result = gather_selections(owner_object, owner_type, node.selections, selections_to_run, next_selections) if !result.equal?(next_selections) selections_to_run = result @@ -177,8 +177,8 @@ def gather_selections(owner_object, owner_type, selections, selections_to_run = end when GraphQL::Language::Nodes::FragmentSpread fragment_def = query.fragments[node.name] - type_defn = query.get_type(fragment_def.type.name) - if query.warden.possible_types(type_defn).include?(owner_type) + type_defn = query.types.type(fragment_def.type.name) + if query.types.possible_types(type_defn).include?(owner_type) result = gather_selections(owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections) if !result.equal?(next_selections) selections_to_run = result @@ -245,7 +245,7 @@ def evaluate_selection(result_name, field_ast_nodes_or_ast_node, selections_resu end field_name = ast_node.name owner_type = selections_result.graphql_result_type - field_defn = query.warden.get_field(owner_type, field_name) + field_defn = query.types.field(owner_type, field_name) # Set this before calling `run_with_directives`, so that the directive can have the latest path runtime_state = get_current_runtime_state @@ -579,7 +579,7 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select resolved_value = value end - possible_types = query.possible_types(current_type) + possible_types = query.types.possible_types(current_type) if !possible_types.include?(resolved_type) parent_type = field.owner_type err_class = current_type::UnresolvedTypeError diff --git a/lib/graphql/execution/lookahead.rb b/lib/graphql/execution/lookahead.rb index 2438017502..90f509c40a 100644 --- a/lib/graphql/execution/lookahead.rb +++ b/lib/graphql/execution/lookahead.rb @@ -108,16 +108,16 @@ def selected? def selection(field_name, selected_type: @selected_type, arguments: nil) next_field_defn = case field_name when String - @query.get_field(selected_type, field_name) + @query.types.field(selected_type, field_name) when Symbol # Try to avoid the `.to_s` below, if possible all_fields = if selected_type.kind.fields? - @query.warden.fields(selected_type) + @query.types.fields(selected_type) else # Handle unions by checking possible - @query.warden + @query.types .possible_types(selected_type) - .map { |t| @query.warden.fields(t) } + .map { |t| @query.types.fields(t) } .tap(&:flatten!) end @@ -128,7 +128,7 @@ def selection(field_name, selected_type: @selected_type, arguments: nil) # Symbol#name is only present on 3.0+ sym_s = field_name.respond_to?(:name) ? field_name.name : field_name.to_s guessed_name = Schema::Member::BuildType.camelize(sym_s) - @query.get_field(selected_type, guessed_name) + @query.types.field(selected_type, guessed_name) end end lookahead_for_selection(next_field_defn, selected_type, arguments) @@ -144,7 +144,7 @@ def alias_selection(alias_name, selected_type: @selected_type, arguments: nil) alias_node = lookup_alias_node(ast_nodes, alias_name) return NULL_LOOKAHEAD unless alias_node - next_field_defn = @query.get_field(selected_type, alias_node.name) + next_field_defn = @query.types.field(selected_type, alias_node.name) alias_arguments = @query.arguments_for(alias_node, next_field_defn) if alias_arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) @@ -183,7 +183,7 @@ def selections(arguments: nil) subselections_by_type.each do |type, ast_nodes_by_response_key| ast_nodes_by_response_key.each do |response_key, ast_nodes| - field_defn = @query.get_field(type, ast_nodes.first.name) + field_defn = @query.types.field(type, ast_nodes.first.name) lookahead = Lookahead.new(query: @query, ast_nodes: ast_nodes, field: field_defn, owner_type: type) subselections.push(lookahead) end @@ -266,7 +266,7 @@ def find_selections(subselections_by_type, selections_on_type, selected_type, as elsif arguments.nil? || arguments.empty? selections_on_type[response_key] = [ast_selection] else - field_defn = @query.get_field(selected_type, ast_selection.name) + field_defn = @query.types.field(selected_type, ast_selection.name) if arguments_match?(arguments, field_defn, ast_selection) selections_on_type[response_key] = [ast_selection] end @@ -276,14 +276,14 @@ def find_selections(subselections_by_type, selections_on_type, selected_type, as subselections_on_type = selections_on_type if (t = ast_selection.type) # Assuming this is valid, that `t` will be found. - on_type = @query.get_type(t.name) + on_type = @query.types.type(t.name) subselections_on_type = subselections_by_type[on_type] ||= {} end find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments) when GraphQL::Language::Nodes::FragmentSpread frag_defn = lookup_fragment(ast_selection) # Again, assuming a valid AST - on_type = @query.get_type(frag_defn.type.name) + on_type = @query.types.type(frag_defn.type.name) subselections_on_type = subselections_by_type[on_type] ||= {} find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments) else diff --git a/lib/graphql/introspection/directive_type.rb b/lib/graphql/introspection/directive_type.rb index e4cf43eb32..72ee132aa8 100644 --- a/lib/graphql/introspection/directive_type.rb +++ b/lib/graphql/introspection/directive_type.rb @@ -22,7 +22,7 @@ class DirectiveType < Introspection::BaseObject field :is_repeatable, Boolean, method: :repeatable? def args(include_deprecated:) - args = @context.warden.arguments(@object) + args = @context.types.arguments(@object) args = args.reject(&:deprecation_reason) unless include_deprecated args end diff --git a/lib/graphql/introspection/entry_points.rb b/lib/graphql/introspection/entry_points.rb index 252997753a..d77cd1c872 100644 --- a/lib/graphql/introspection/entry_points.rb +++ b/lib/graphql/introspection/entry_points.rb @@ -15,8 +15,8 @@ def __schema end def __type(name:) - if context.warden.reachable_type?(name) - context.warden.get_type(name) + if context.types.reachable_type?(name) + context.types.type(name) elsif (type = context.schema.extra_types.find { |t| t.graphql_name == name }) type else diff --git a/lib/graphql/introspection/field_type.rb b/lib/graphql/introspection/field_type.rb index 4ca8c1de1f..681bb3a705 100644 --- a/lib/graphql/introspection/field_type.rb +++ b/lib/graphql/introspection/field_type.rb @@ -19,7 +19,7 @@ def is_deprecated end def args(include_deprecated:) - args = @context.warden.arguments(@object) + args = @context.types.arguments(@object) args = args.reject(&:deprecation_reason) unless include_deprecated args end diff --git a/lib/graphql/introspection/schema_type.rb b/lib/graphql/introspection/schema_type.rb index d317d0b3ee..a397b58a4b 100644 --- a/lib/graphql/introspection/schema_type.rb +++ b/lib/graphql/introspection/schema_type.rb @@ -20,7 +20,8 @@ def schema_description end def types - types = context.warden.reachable_types + context.schema.extra_types + query_types = context.types.all_types + types = query_types + context.schema.extra_types types.sort_by!(&:graphql_name) types end @@ -38,13 +39,22 @@ def subscription_type end def directives - @context.warden.directives + @context.types.directives end private def permitted_root_type(op_type) - @context.warden.root_type_for_operation(op_type) + case op_type + when "query" + @context.types.query_root + when "mutation" + @context.types.mutation_root + when "subcription" + @context.types.subscription_root + else + nil + end end end end diff --git a/lib/graphql/introspection/type_type.rb b/lib/graphql/introspection/type_type.rb index 4d7b88a0da..9e2ede5948 100644 --- a/lib/graphql/introspection/type_type.rb +++ b/lib/graphql/introspection/type_type.rb @@ -52,7 +52,7 @@ def enum_values(include_deprecated:) if !@object.kind.enum? nil else - enum_values = @context.warden.enum_values(@object) + enum_values = @context.types.enum_values(@object) if !include_deprecated enum_values = enum_values.select {|f| !f.deprecation_reason } @@ -64,7 +64,7 @@ def enum_values(include_deprecated:) def interfaces if @object.kind.object? || @object.kind.interface? - @context.warden.interfaces(@object).sort_by(&:graphql_name) + @context.types.interfaces(@object).sort_by(&:graphql_name) else nil end @@ -72,7 +72,7 @@ def interfaces def input_fields(include_deprecated:) if @object.kind.input_object? - args = @context.warden.arguments(@object) + args = @context.types.arguments(@object) args = args.reject(&:deprecation_reason) unless include_deprecated args else @@ -82,7 +82,7 @@ def input_fields(include_deprecated:) def possible_types if @object.kind.abstract? - @context.warden.possible_types(@object).sort_by(&:graphql_name) + @context.types.possible_types(@object).sort_by(&:graphql_name) else nil end @@ -92,7 +92,7 @@ def fields(include_deprecated:) if !@object.kind.fields? nil else - fields = @context.warden.fields(@object) + fields = @context.types.fields(@object) if !include_deprecated fields = fields.select {|f| !f.deprecation_reason } end diff --git a/lib/graphql/language/document_from_schema_definition.rb b/lib/graphql/language/document_from_schema_definition.rb index d47c20156d..2c4426ead1 100644 --- a/lib/graphql/language/document_from_schema_definition.rb +++ b/lib/graphql/language/document_from_schema_definition.rb @@ -24,15 +24,8 @@ def initialize( @include_built_in_directives = include_built_in_directives @include_one_of = false - schema_context = schema.context_class.new(query: nil, schema: schema, values: context) - - - @warden = @schema.warden_class.new( - schema: @schema, - context: schema_context, - ) - - schema_context.warden = @warden + dummy_query = @schema.query_class.new(@schema, "{ __typename }", validate: false, context: context) + @types = dummy_query.types # rubocop:disable Development/ContextIsPassedCop end def document @@ -44,9 +37,9 @@ def document def build_schema_node if !schema_respects_root_name_conventions?(@schema) GraphQL::Language::Nodes::SchemaDefinition.new( - query: (q = warden.root_type_for_operation("query")) && q.graphql_name, - mutation: (m = warden.root_type_for_operation("mutation")) && m.graphql_name, - subscription: (s = warden.root_type_for_operation("subscription")) && s.graphql_name, + query: @types.query_root&.graphql_name, + mutation: @types.mutation_root&.graphql_name, + subscription: @types.subscription_root&.graphql_name, directives: definition_directives(@schema, :schema_directives) ) else @@ -57,7 +50,7 @@ def build_schema_node end def build_object_type_node(object_type) - ints = warden.interfaces(object_type) + ints = @types.interfaces(object_type) if ints.any? ints.sort_by!(&:graphql_name) ints.map! { |iface| build_type_name_node(iface) } @@ -66,7 +59,7 @@ def build_object_type_node(object_type) GraphQL::Language::Nodes::ObjectTypeDefinition.new( name: object_type.graphql_name, interfaces: ints, - fields: build_field_nodes(warden.fields(object_type)), + fields: build_field_nodes(@types.fields(object_type)), description: object_type.description, directives: directives(object_type), ) @@ -75,7 +68,7 @@ def build_object_type_node(object_type) def build_field_node(field) GraphQL::Language::Nodes::FieldDefinition.new( name: field.graphql_name, - arguments: build_argument_nodes(warden.arguments(field)), + arguments: build_argument_nodes(@types.arguments(field)), type: build_type_name_node(field.type), description: field.description, directives: directives(field), @@ -86,7 +79,7 @@ def build_union_type_node(union_type) GraphQL::Language::Nodes::UnionTypeDefinition.new( name: union_type.graphql_name, description: union_type.description, - types: warden.possible_types(union_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, + types: @types.possible_types(union_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, directives: directives(union_type), ) end @@ -94,9 +87,9 @@ def build_union_type_node(union_type) def build_interface_type_node(interface_type) GraphQL::Language::Nodes::InterfaceTypeDefinition.new( name: interface_type.graphql_name, - interfaces: warden.interfaces(interface_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, + interfaces: @types.interfaces(interface_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) }, description: interface_type.description, - fields: build_field_nodes(warden.fields(interface_type)), + fields: build_field_nodes(@types.fields(interface_type)), directives: directives(interface_type), ) end @@ -104,7 +97,7 @@ def build_interface_type_node(interface_type) def build_enum_type_node(enum_type) GraphQL::Language::Nodes::EnumTypeDefinition.new( name: enum_type.graphql_name, - values: warden.enum_values(enum_type).sort_by(&:graphql_name).map do |enum_value| + values: @types.enum_values(enum_type).sort_by(&:graphql_name).map do |enum_value| build_enum_value_node(enum_value) end, description: enum_type.description, @@ -149,7 +142,7 @@ def build_argument_node(argument) def build_input_object_node(input_object) GraphQL::Language::Nodes::InputObjectTypeDefinition.new( name: input_object.graphql_name, - fields: build_argument_nodes(warden.arguments(input_object)), + fields: build_argument_nodes(@types.arguments(input_object)), description: input_object.description, directives: directives(input_object), ) @@ -159,7 +152,7 @@ def build_directive_node(directive) GraphQL::Language::Nodes::DirectiveDefinition.new( name: directive.graphql_name, repeatable: directive.repeatable?, - arguments: build_argument_nodes(warden.arguments(directive)), + arguments: build_argument_nodes(@types.arguments(directive)), locations: build_directive_location_nodes(directive.locations), description: directive.description, ) @@ -204,7 +197,7 @@ def build_default_value(default_value, type) when "INPUT_OBJECT" GraphQL::Language::Nodes::InputObject.new( arguments: default_value.to_h.map do |arg_name, arg_value| - args = @warden.arguments(type) + args = @types.arguments(type) arg = args.find { |a| a.keyword.to_s == arg_name.to_s } if arg.nil? raise ArgumentError, "No argument definition on #{type.graphql_name} for argument: #{arg_name.inspect} (expected one of: #{args.map(&:keyword)})" @@ -260,13 +253,13 @@ def build_directive_nodes(directives) end def build_definition_nodes - dirs_to_build = warden.directives + dirs_to_build = @types.directives if !include_built_in_directives dirs_to_build = dirs_to_build.reject { |directive| directive.default_directive? } end definitions = build_directive_nodes(dirs_to_build) - - type_nodes = build_type_definition_nodes(warden.reachable_types + schema.extra_types) + all_types = @types.all_types + type_nodes = build_type_definition_nodes(all_types + schema.extra_types) if @include_one_of # This may have been set to true when iterating over all types definitions.concat(build_directive_nodes([GraphQL::Schema::Directive::OneOf])) @@ -351,7 +344,7 @@ def definition_directives(member, directives_method) dirs end - attr_reader :schema, :warden, :always_include_schema, + attr_reader :schema, :always_include_schema, :include_introspection_types, :include_built_in_directives, :include_built_in_scalars end end diff --git a/lib/graphql/language/sanitized_printer.rb b/lib/graphql/language/sanitized_printer.rb index 5b6a8e452a..82d576ff63 100644 --- a/lib/graphql/language/sanitized_printer.rb +++ b/lib/graphql/language/sanitized_printer.rb @@ -113,7 +113,7 @@ def print_variable_identifier(variable_id) end def print_field(field, indent: "") - @current_field = query.get_field(@current_type, field.name) + @current_field = query.types.field(@current_type, field.name) old_type = @current_type @current_type = @current_field.type.unwrap super diff --git a/lib/graphql/query.rb b/lib/graphql/query.rb index 46ea7479fa..237b8b81a1 100644 --- a/lib/graphql/query.rb +++ b/lib/graphql/query.rb @@ -95,12 +95,24 @@ def selected_operation_name # @param root_value [Object] the object used to resolve fields on the root type # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value) # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value) - def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, validate: true, static_validator: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil) + def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, validate: true, static_validator: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil, use_schema_subset: nil) # Even if `variables: nil` is passed, use an empty hash for simpler logic variables ||= {} @schema = schema @context = schema.context_class.new(query: self, values: context) - @warden = warden + + if use_schema_subset.nil? + use_schema_subset = warden ? false : schema.use_schema_subset? + end + + if use_schema_subset + @schema_subset = @schema.subset_class.new(self) + @warden = Schema::Warden::NullWarden.new(context: self, schema: @schema) + else + @schema_subset = nil + @warden = warden + end + @subscription_topic = subscription_topic @root_value = root_value @fragments = nil @@ -195,7 +207,14 @@ def subscription_update? def lookahead @lookahead ||= begin ast_node = selected_operation - root_type = warden.root_type_for_operation(ast_node.operation_type || "query") + root_type = case ast_node.operation_type + when nil, "query" + types.query_root # rubocop:disable Development/ContextIsPassedCop + when "mutation" + types.mutation_root # rubocop:disable Development/ContextIsPassedCop + when "subscription" + types.subscription_root # rubocop:disable Development/ContextIsPassedCop + end GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node]) end end @@ -330,6 +349,10 @@ def warden def_delegators :warden, :get_type, :get_field, :possible_types, :root_type_for_operation + def types + @schema_subset || warden.schema_subset + end + # @param abstract_type [GraphQL::UnionType, GraphQL::InterfaceType] # @param value [Object] Any runtime value # @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type} diff --git a/lib/graphql/query/context.rb b/lib/graphql/query/context.rb index 04e5e4705c..04c77e5184 100644 --- a/lib/graphql/query/context.rb +++ b/lib/graphql/query/context.rb @@ -84,6 +84,10 @@ def []=(key, value) def_delegators :@query, :trace, :interpreter? + def types + @query.types + end + RUNTIME_METADATA_KEYS = Set.new([:current_object, :current_arguments, :current_field, :current_path]) # @!method []=(key, value) # Reassign `key` to the hash passed to {Schema#execute} as `context:` diff --git a/lib/graphql/query/null_context.rb b/lib/graphql/query/null_context.rb index d23fa6bfe7..f620d182df 100644 --- a/lib/graphql/query/null_context.rb +++ b/lib/graphql/query/null_context.rb @@ -30,6 +30,10 @@ def initialize def interpreter? true end + + def types + @types ||= GraphQL::Schema::Warden::SchemaSubset.new(@warden) + end end end end diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb index c609bf4adb..cd0d357649 100644 --- a/lib/graphql/schema.rb +++ b/lib/graphql/schema.rb @@ -45,6 +45,7 @@ require "graphql/schema/has_single_input_argument" require "graphql/schema/relay_classic_mutation" require "graphql/schema/subscription" +require "graphql/schema/subset" module GraphQL # A GraphQL schema which may be queried with {GraphQL::Query}. @@ -369,20 +370,24 @@ def get_type(type_name, context = GraphQL::Query::NullContext.instance) when nil nil when Array - visible_t = nil - warden = Warden.from_context(context) - local_entry.each do |t| - if warden.visible_type?(t, context) - if visible_t.nil? - visible_t = t - else - raise DuplicateNamesError.new( - duplicated_name: type_name, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect - ) + if context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset) + local_entry + else + visible_t = nil + warden = Warden.from_context(context) + local_entry.each do |t| + if warden.visible_type?(t, context) + if visible_t.nil? + visible_t = t + else + raise DuplicateNamesError.new( + duplicated_name: type_name, duplicated_definition_1: visible_t.inspect, duplicated_definition_2: t.inspect + ) + end end end + visible_t end - visible_t when Module local_entry else @@ -496,6 +501,28 @@ def warden_class attr_writer :warden_class + def subset_class + if defined?(@subset_class) + @subset_class + elsif superclass.respond_to?(:subset_class) + superclass.subset_class + else + GraphQL::Schema::Subset + end + end + + attr_writer :subset_class, :use_schema_subset + + def use_schema_subset? + if defined?(@use_schema_subset) + @use_schema_subset + elsif superclass.respond_to?(:use_schema_subset?) + superclass.use_schema_subset? + else + false + end + end + # @param type [Module] The type definition whose possible types you want to see # @return [Hash] All possible types, if no `type` is given. # @return [Array] Possible types for `type`, if it's given. @@ -572,9 +599,8 @@ def references_to(to_type = nil, from: nil) end end - def type_from_ast(ast_node, context: nil) - type_owner = context ? context.warden : self - GraphQL::Schema::TypeExpression.build_type(type_owner, ast_node) + def type_from_ast(ast_node, context: self.query_class.new(self, "{ __typename }").context) + GraphQL::Schema::TypeExpression.build_type(context.query.types, ast_node) end def get_field(type_or_name, field_name, context = GraphQL::Query::NullContext.instance) @@ -1052,6 +1078,10 @@ def schema_directives Member::HasDirectives.get_directives(self, @own_schema_directives, :schema_directives) end + # Called when a type is needed by name at runtime + def load_type(type_name, ctx) + get_type(type_name, ctx) + end # This hook is called when an object fails an `authorized?` check. # You might report to your bug tracker here, so you can correct # the field resolvers not to return unauthorized objects. diff --git a/lib/graphql/schema/always_visible.rb b/lib/graphql/schema/always_visible.rb index 6f9ea727cf..6feadc123c 100644 --- a/lib/graphql/schema/always_visible.rb +++ b/lib/graphql/schema/always_visible.rb @@ -4,6 +4,7 @@ class Schema class AlwaysVisible def self.use(schema, **opts) schema.warden_class = GraphQL::Schema::Warden::NullWarden + schema.subset_class = GraphQL::Schema::Warden::NullWarden::NullSubset end end end diff --git a/lib/graphql/schema/enum.rb b/lib/graphql/schema/enum.rb index b0492f01d5..83b87a73ff 100644 --- a/lib/graphql/schema/enum.rb +++ b/lib/graphql/schema/enum.rb @@ -130,7 +130,7 @@ def kind end def validate_non_null_input(value_name, ctx, max_errors: nil) - allowed_values = ctx.warden.enum_values(self) + allowed_values = ctx.types.enum_values(self) matching_value = allowed_values.find { |v| v.graphql_name == value_name } if matching_value.nil? @@ -141,8 +141,8 @@ def validate_non_null_input(value_name, ctx, max_errors: nil) end def coerce_result(value, ctx) - warden = ctx.warden - all_values = warden ? warden.enum_values(self) : values.each_value + types = ctx.types + all_values = types ? types.enum_values(self) : values.each_value enum_value = all_values.find { |val| val.value == value } if enum_value enum_value.graphql_name @@ -152,7 +152,7 @@ def coerce_result(value, ctx) end def coerce_input(value_name, ctx) - all_values = ctx.warden ? ctx.warden.enum_values(self) : values.each_value + all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value if v = all_values.find { |val| val.graphql_name == value_name } v.value diff --git a/lib/graphql/schema/field.rb b/lib/graphql/schema/field.rb index 84376eacae..b1863c9fd7 100644 --- a/lib/graphql/schema/field.rb +++ b/lib/graphql/schema/field.rb @@ -618,7 +618,7 @@ def authorized?(object, args, context) using_arg_values = false end - args = context.warden.arguments(self) + args = context.types.arguments(self) args.each do |arg| arg_key = arg.keyword if arg_values.key?(arg_key) diff --git a/lib/graphql/schema/has_single_input_argument.rb b/lib/graphql/schema/has_single_input_argument.rb index 83d7e55167..26c266840f 100644 --- a/lib/graphql/schema/has_single_input_argument.rb +++ b/lib/graphql/schema/has_single_input_argument.rb @@ -149,7 +149,8 @@ def description(new_desc = nil) def authorize_arguments(args, values) # remove the `input` wrapper to match values - input_args = args["input"].type.unwrap.arguments(context) + input_type = args.find { |a| a.graphql_name == "input" }.type.unwrap + input_args = context.types.arguments(input_type) super(input_args, values) end end diff --git a/lib/graphql/schema/input_object.rb b/lib/graphql/schema/input_object.rb index 1b4073ecc8..d04b0289e4 100644 --- a/lib/graphql/schema/input_object.rb +++ b/lib/graphql/schema/input_object.rb @@ -23,7 +23,8 @@ def initialize(arguments, ruby_kwargs:, context:, defaults_used:) @ruby_style_hash = ruby_kwargs @arguments = arguments # Apply prepares, not great to have it duplicated here. - self.class.arguments(context).each_value do |arg_defn| + arg_defns = context ? context.types.arguments(self.class) : self.class.arguments(context).each_value + arg_defns.each do |arg_defn| ruby_kwargs_key = arg_defn.keyword if @ruby_style_hash.key?(ruby_kwargs_key) # Weirdly, procs are applied during coercion, but not methods. @@ -58,7 +59,7 @@ def prepare def self.authorized?(obj, value, ctx) # Authorize each argument (but this doesn't apply if `prepare` is implemented): if value.respond_to?(:key?) - arguments(ctx).each do |_name, input_obj_arg| + ctx.types.arguments(self).each do |input_obj_arg| if value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx) return false @@ -149,7 +150,7 @@ def kind INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object." def validate_non_null_input(input, ctx, max_errors: nil) - warden = ctx.warden + types = ctx.types if input.is_a?(Array) return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) }) @@ -161,9 +162,9 @@ def validate_non_null_input(input, ctx, max_errors: nil) end # Inject missing required arguments - missing_required_inputs = self.arguments(ctx).reduce({}) do |m, (argument_name, argument)| - if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name) - m[argument_name] = nil + missing_required_inputs = ctx.types.arguments(self).reduce({}) do |m, (argument)| + if !input.key?(argument.graphql_name) && argument.type.non_null? && types.argument(self, argument.graphql_name) + m[argument.graphql_name] = nil end m @@ -172,7 +173,7 @@ def validate_non_null_input(input, ctx, max_errors: nil) result = nil [input, missing_required_inputs].each do |args_to_validate| args_to_validate.each do |argument_name, value| - argument = warden.get_argument(self, argument_name) + argument = types.argument(self, argument_name) # Items in the input that are unexpected if argument.nil? result ||= Query::InputValidationResult.new diff --git a/lib/graphql/schema/introspection_system.rb b/lib/graphql/schema/introspection_system.rb index dee9a3554b..a9053b0e69 100644 --- a/lib/graphql/schema/introspection_system.rb +++ b/lib/graphql/schema/introspection_system.rb @@ -69,7 +69,7 @@ def dynamic_field(name:) def resolve_late_bindings @types.each do |name, t| if t.kind.fields? - t.fields.each do |_name, field_defn| + t.all_field_definitions.each do |field_defn| field_defn.type = resolve_late_binding(field_defn.type) end end @@ -113,19 +113,7 @@ def load_constant(class_name) def get_fields_from_class(class_sym:) object_type_defn = load_constant(class_sym) - - if object_type_defn.is_a?(Module) - object_type_defn.fields - else - extracted_field_defns = {} - object_class = object_type_defn.metadata[:type_class] - object_type_defn.all_fields.each do |field_defn| - inner_resolve = field_defn.resolve_proc - resolve_with_instantiate = PerFieldProxyResolve.new(object_class: object_class, inner_resolve: inner_resolve) - extracted_field_defns[field_defn.name] = field_defn.redefine(resolve: resolve_with_instantiate) - end - extracted_field_defns - end + object_type_defn.fields end # This is probably not 100% robust -- but it has to be good enough to avoid modifying the built-in introspection types diff --git a/lib/graphql/schema/member/has_arguments.rb b/lib/graphql/schema/member/has_arguments.rb index e945521fec..626332382c 100644 --- a/lib/graphql/schema/member/has_arguments.rb +++ b/lib/graphql/schema/member/has_arguments.rb @@ -135,10 +135,11 @@ def all_argument_definitions def get_argument(argument_name, context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) + skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset) for ancestor in ancestors if ancestor.respond_to?(:own_arguments) && (a = ancestor.own_arguments[argument_name]) && - (a = Warden.visible_entry?(:visible_argument?, a, context, warden)) + (skip_visible || (a = Warden.visible_entry?(:visible_argument?, a, context, warden))) return a end end @@ -205,8 +206,8 @@ def all_argument_definitions # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name. def get_argument(argument_name, context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) - if (arg_config = own_arguments[argument_name]) && (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden)) - visible_arg + if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden))) + visible_arg || arg_config elsif defined?(@resolver_class) && @resolver_class @resolver_class.get_field_argument(argument_name, context) else @@ -230,7 +231,7 @@ def argument_class(new_arg_class = nil) # @return [Interpreter::Arguments, Execution::Lazy] def coerce_arguments(parent_object, values, context, &block) # Cache this hash to avoid re-merging it - arg_defns = context.warden.arguments(self) + arg_defns = context.types.arguments(self) total_args_count = arg_defns.size finished_args = nil @@ -364,8 +365,8 @@ def authorize_application_object(argument, id, context, loaded_application_objec end if !( - context.warden.possible_types(argument.loads).include?(application_object_type) || - context.warden.loadable?(argument.loads, context) + context.types.possible_types(argument.loads).include?(application_object_type) || + context.types.loadable?(argument.loads, context) ) err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object) application_object = load_application_object_failed(err) diff --git a/lib/graphql/schema/member/has_fields.rb b/lib/graphql/schema/member/has_fields.rb index 0b99af19af..ef18af8dc7 100644 --- a/lib/graphql/schema/member/has_fields.rb +++ b/lib/graphql/schema/member/has_fields.rb @@ -99,11 +99,12 @@ def all_field_definitions module InterfaceMethods def get_field(field_name, context = GraphQL::Query::NullContext.instance) warden = Warden.from_context(context) + skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset) for ancestor in ancestors if ancestor.respond_to?(:own_fields) && (f_entry = ancestor.own_fields[field_name]) && - (f = Warden.visible_entry?(:visible_field?, f_entry, context, warden)) - return f + (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden))) + return f_entry end end nil @@ -134,13 +135,14 @@ def get_field(field_name, context = GraphQL::Query::NullContext.instance) # Objects need to check that the interface implementation is visible, too warden = Warden.from_context(context) ancs = ancestors + skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Subset) i = 0 while (ancestor = ancs[i]) if ancestor.respond_to?(:own_fields) && visible_interface_implementation?(ancestor, context, warden) && (f_entry = ancestor.own_fields[field_name]) && - (f = Warden.visible_entry?(:visible_field?, f_entry, context, warden)) - return f + (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden))) + return f_entry end i += 1 end diff --git a/lib/graphql/schema/resolver.rb b/lib/graphql/schema/resolver.rb index 1796975868..ce5e746885 100644 --- a/lib/graphql/schema/resolver.rb +++ b/lib/graphql/schema/resolver.rb @@ -36,7 +36,7 @@ def initialize(object:, context:, field:) @field = field # Since this hash is constantly rebuilt, cache it for this call @arguments_by_keyword = {} - self.class.arguments(context).each do |name, arg| + context.types.arguments(self.class).each do |arg| @arguments_by_keyword[arg.keyword] = arg end @prepared_arguments = nil @@ -152,7 +152,7 @@ def ready?(**args) # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.) def authorized?(**inputs) arg_owner = @field # || self.class - args = arg_owner.arguments(context) + args = context.types.arguments(arg_owner) authorize_arguments(args, inputs) end @@ -169,7 +169,7 @@ def unauthorized_object(err) private def authorize_arguments(args, inputs) - args.each_value do |argument| + args.each do |argument| arg_keyword = argument.keyword if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value) auth_result = argument.authorized?(self, arg_value, context) @@ -182,10 +182,9 @@ def authorize_arguments(args, inputs) elsif auth_result == false return auth_result end - else - true end end + true end def load_arguments(args) diff --git a/lib/graphql/schema/subset.rb b/lib/graphql/schema/subset.rb new file mode 100644 index 0000000000..7c089c4a3b --- /dev/null +++ b/lib/graphql/schema/subset.rb @@ -0,0 +1,397 @@ +# frozen_string_literal: true + +module GraphQL + class Schema + class Subset + def initialize(query) + @query = query + @context = query.context + @schema = query.schema + @all_types = {} + @all_types_loaded = false + @unvisited_types = [] + @referenced_types = Hash.new { |h, type_defn| h[type_defn] = [] }.compare_by_identity + @cached_possible_types = nil + @cached_visible = Hash.new { |h, member| + h[member] = @schema.visible?(member, @context) + }.compare_by_identity + + @cached_visible_fields = Hash.new { |h, owner| + h[owner] = Hash.new do |h2, field| + h2[field] = if @cached_visible[field] && + (ret_type = field.type.unwrap) && + @cached_visible[ret_type] && + reachable_type?(ret_type.graphql_name) && + (owner == field.owner || (!owner.kind.object?) || field_on_visible_interface?(field, owner)) + + if !field.introspection? + # The problem is that some introspection fields may have references + # to non-custom introspection types. + # If those were added here, they'd cause a DuplicateNamesError. + # This is basically a bug -- those fields _should_ reference the custom types. + add_type(ret_type, field) + end + true + else + false + end + end.compare_by_identity + }.compare_by_identity + + @cached_visible_arguments = Hash.new do |h, arg| + h[arg] = if @cached_visible[arg] && (arg_type = arg.type.unwrap) && @cached_visible[arg_type] + add_type(arg_type, arg) + true + else + false + end + end.compare_by_identity + + @unfiltered_pt = Hash.new do |hash, type| + hash[type] = @schema.possible_types(type) + end.compare_by_identity + end + + def field_on_visible_interface?(field, owner) + ints = owner.interface_type_memberships.map(&:abstract_type) + field_name = field.graphql_name + filtered_ints = interfaces(owner) + any_interface_has_field = false + any_interface_has_visible_field = false + ints.each do |int_t| + if (_int_f_defn = int_t.get_field(field_name, @context)) + any_interface_has_field = true + + if filtered_ints.include?(int_t) # TODO cycles, or maybe not necessary since previously checked? && @cached_visible_fields[owner][field] + any_interface_has_visible_field = true + break + end + end + end + + if any_interface_has_field + any_interface_has_visible_field + else + true + end + end + + def type(type_name) + t = if (loaded_t = @all_types[type_name]) + loaded_t + elsif !@all_types_loaded + load_all_types + @all_types[type_name] + end + + if t + if t.is_a?(Array) + vis_t = nil + t.each do |t_defn| + if @cached_visible[t_defn] + if vis_t.nil? + vis_t = t_defn + else + raise_duplicate_definition(vis_t, t_defn) + end + end + end + vis_t + else + if t && @cached_visible[t] + t + else + nil + end + end + end + end + + def field(owner, field_name) + f = if owner.kind.fields? && (field = owner.get_field(field_name, @context)) + field + elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name)) + entry_point_field + elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name)) + dynamic_field + else + nil + end + if f.is_a?(Array) + visible_f = nil + f.each do |f_defn| + if @cached_visible_fields[owner][f_defn] + + if visible_f.nil? + visible_f = f_defn + else + raise_duplicate_definition(visible_f, f_defn) + end + end + end + visible_f + else + if f && @cached_visible_fields[owner][f] + f + else + nil + end + end + end + + def fields(owner) + non_duplicate_items(owner.all_field_definitions, @cached_visible_fields[owner]) + end + + def arguments(owner) + non_duplicate_items(owner.all_argument_definitions, @cached_visible_arguments) + end + + def argument(owner, arg_name) + # TODO this makes a Warden.visible_entry call down the stack + # I need a non-Warden implementation + arg = owner.get_argument(arg_name, @context) + if arg.is_a?(Array) + visible_arg = nil + arg.each do |arg_defn| + if @cached_visible_arguments[arg_defn] + if arg_defn&.loads + add_type(arg_defn.loads, arg_defn) + end + if visible_arg.nil? + visible_arg = arg_defn + else + raise_duplicate_definition(visible_arg, arg_defn) + end + end + end + visible_arg + else + if arg && @cached_visible_arguments[arg] + if arg&.loads + add_type(arg.loads, arg) + end + arg + else + nil + end + end + end + + def possible_types(type) + @cached_possible_types ||= Hash.new do |h, type| + pt = case type.kind.name + when "INTERFACE" + # TODO this requires the global map + @unfiltered_pt[type] + when "UNION" + type.type_memberships.select { |tm| @cached_visible[tm] && @cached_visible[tm.object_type] }.map!(&:object_type) + else + [type] + end + + # TODO use `select!` when possible, skip it for `[type]` + h[type] = pt.select { |t| + @cached_visible[t] && referenced?(t) + } + end.compare_by_identity + @cached_possible_types[type] + end + + def interfaces(obj_or_int_type) + ints = obj_or_int_type.interface_type_memberships + .select { |itm| @cached_visible[itm] && @cached_visible[itm.abstract_type] } + .map!(&:abstract_type) + ints.uniq! # Remove any duplicate interfaces implemented via other interfaces + ints + end + + def query_root + add_if_visible(@schema.query) + end + + def mutation_root + add_if_visible(@schema.mutation) + end + + def subscription_root + add_if_visible(@schema.subscription) + end + + def all_types + @all_types_filtered ||= begin + load_all_types + at = [] + @all_types.each do |_name, type_defn| + if possible_types(type_defn).any? || referenced?(type_defn) + at << type_defn + end + end + at + end + end + + def enum_values(owner) + values = non_duplicate_items(owner.all_enum_value_definitions, @cached_visible) + if values.size == 0 + raise GraphQL::Schema::Enum::MissingValuesError.new(owner) + end + values + end + + def directive_exists?(dir_name) + dir = @schema.directives[dir_name] + dir && @cached_visible[dir] + end + + def directives + @schema.directives.each_value.select { |d| @cached_visible[d] } + end + + def loadable?(t, _ctx) + !@all_types[t.graphql_name] # TODO make sure t is not reachable but t is visible + end + + # TODO rename this to indicate that it is called with a typename + def reachable_type?(type_name) + load_all_types + !!((t = @all_types[type_name]) && referenced?(t)) + end + + def loaded_types + @all_types.values + end + + private + + def add_if_visible(t) + (t && @cached_visible[t]) ? (add_type(t, true); t) : nil + end + + def add_type(t, by_member) + if t && @cached_visible[t] + n = t.graphql_name + if (prev_t = @all_types[n]) + if !prev_t.equal?(t) + raise_duplicate_definition(prev_t, t) + end + false + else + @referenced_types[t] << by_member + @all_types[n] = t + @unvisited_types << t + true + end + else + false + end + end + + def non_duplicate_items(definitions, visibility_cache) + non_dups = [] + definitions.each do |defn| + if visibility_cache[defn] + if (dup_defn = non_dups.find { |d| d.graphql_name == defn.graphql_name }) + raise_duplicate_definition(dup_defn, defn) + end + non_dups << defn + end + end + non_dups + end + + def raise_duplicate_definition(first_defn, second_defn) + raise DuplicateNamesError.new(duplicated_name: first_defn.path, duplicated_definition_1: first_defn.inspect, duplicated_definition_2: second_defn.inspect) + end + + def referenced?(t) + load_all_types + res = if @referenced_types[t].any? { |member| (member == true) || @cached_visible[member] } + if t.kind.abstract? + possible_types(t).any? + else + true + end + end + res + end + + def load_all_types + return if @all_types_loaded + @all_types_loaded = true + schema_types = [ + query_root, + mutation_root, + subscription_root, + *@schema.introspection_system.types.values, + ] + + # Don't include any orphan_types whose interfaces aren't visible. + @schema.orphan_types.each do |orphan_type| + if @cached_visible[orphan_type] && + orphan_type.interface_type_memberships.any? { |tm| @cached_visible[tm] && @cached_visible[tm.abstract_type] } + schema_types << orphan_type + end + end + schema_types.compact! # TODO why is this necessary?! + schema_types.flatten! # handle multiple defns + schema_types.each { |t| add_type(t, true) } + + while t = @unvisited_types.pop + # These have already been checked for `.visible?` + visit_type(t) + end + + @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) } + nil + end + + def visit_type(type) + if type.kind.input_object? + # recurse into visible arguments + arguments(type).each do |argument| + add_type(argument.type.unwrap, argument) + end + elsif type.kind.union? + # recurse into visible possible types + type.type_memberships.each do |tm| + if @cached_visible[tm] && @cached_visible[tm.object_type] + add_type(tm.object_type, tm) + end + end + elsif type.kind.fields? + if type.kind.object? + # recurse into visible implemented interfaces + interfaces(type).each do |interface| + add_type(interface, type) + end + end + + # recurse into visible fields + t_f = type.all_field_definitions + t_f.each do |field| + if @cached_visible[field] + field_type = field.type.unwrap + if field_type.kind.interface? + pt = @unfiltered_pt[field_type] + pt.each do |obj_type| + if @cached_visible[obj_type] && + (tm = obj_type.interface_type_memberships.find { |tm| tm.abstract_type == field_type }) && + @cached_visible[tm] + add_type(obj_type, tm) + end + end + end + add_type(field_type, field) + + # recurse into visible arguments + arguments(field).each do |argument| + add_type(argument.type.unwrap, argument) + end + end + end + end + end + end + end +end diff --git a/lib/graphql/schema/type_expression.rb b/lib/graphql/schema/type_expression.rb index c7ed4ad534..5ade5c7014 100644 --- a/lib/graphql/schema/type_expression.rb +++ b/lib/graphql/schema/type_expression.rb @@ -5,13 +5,13 @@ class Schema module TypeExpression # Fetch a type from a type map by its AST specification. # Return `nil` if not found. - # @param type_owner [#get_type] A thing for looking up types by name + # @param type_owner [#type] A thing for looking up types by name # @param ast_node [GraphQL::Language::Nodes::AbstractNode] # @return [Class, GraphQL::Schema::NonNull, GraphQL::Schema:List] def self.build_type(type_owner, ast_node) case ast_node when GraphQL::Language::Nodes::TypeName - type_owner.get_type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware + type_owner.type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware when GraphQL::Language::Nodes::NonNullType ast_inner_type = ast_node.of_type inner_type = build_type(type_owner, ast_inner_type) diff --git a/lib/graphql/schema/warden.rb b/lib/graphql/schema/warden.rb index 96f11ab162..f7f8ead232 100644 --- a/lib/graphql/schema/warden.rb +++ b/lib/graphql/schema/warden.rb @@ -61,14 +61,27 @@ def visible_type_membership?(tm, ctx); tm.visible?(ctx); end def interface_type_memberships(obj_t, ctx); obj_t.interface_type_memberships; end def arguments(owner, ctx); owner.arguments(ctx); end def loadable?(type, ctx); type.visible?(ctx); end + def schema_subset + @schema_subset ||= Warden::SchemaSubset.new(self) + end end end class NullWarden def initialize(_filter = nil, context:, schema:) @schema = schema + @schema_subset = Warden::SchemaSubset.new(self) + end + + # @api private + module NullSubset + def self.new(query) + NullWarden.new(context: query.context, schema: query.schema).schema_subset + end end + attr_reader :schema_subset + def visible_field?(field_defn, _ctx = nil, owner = nil); true; end def visible_argument?(arg_defn, _ctx = nil); true; end def visible_type?(type_defn, _ctx = nil); true; end @@ -91,6 +104,80 @@ def possible_types(type_defn); @schema.possible_types(type_defn); end def interfaces(obj_type); obj_type.interfaces; end end + def schema_subset + @schema_subset ||= SchemaSubset.new(self) + end + + class SchemaSubset + def initialize(warden) + @warden = warden + end + + def directives + @warden.directives + end + + def directive_exists?(dir_name) + @warden.directives.any? { |d| d.graphql_name == dir_name } + end + + def type(name) + @warden.get_type(name) + end + + def field(owner, field_name) + @warden.get_field(owner, field_name) + end + + def argument(owner, arg_name) + @warden.get_argument(owner, arg_name) + end + + def query_root + @warden.root_type_for_operation("query") + end + + def mutation_root + @warden.root_type_for_operation("mutation") + end + + def subscription_root + @warden.root_type_for_operation("subscription") + end + + def arguments(owner) + @warden.arguments(owner) + end + + def fields(owner) + @warden.fields(owner) + end + + def possible_types(type) + @warden.possible_types(type) + end + + def enum_values(enum_type) + @warden.enum_values(enum_type) + end + + def all_types + @warden.reachable_types + end + + def interfaces(obj_type) + @warden.interfaces(obj_type) + end + + def loadable?(t, ctx) # TODO remove ctx here? + @warden.loadable?(t, ctx) + end + + def reachable_type?(type_name) + @warden.reachable_type?(type_name) + end + end + # @param context [GraphQL::Query::Context] # @param schema [GraphQL::Schema] def initialize(context:, schema:) @@ -107,7 +194,7 @@ def initialize(context:, schema:) @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays = @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships = @visible_and_reachable_type = @unions = @unfiltered_interfaces = - @reachable_type_set = + @reachable_type_set = @schema_subset = nil end diff --git a/lib/graphql/static_validation/base_visitor.rb b/lib/graphql/static_validation/base_visitor.rb index 92607c28ae..a73ee6566b 100644 --- a/lib/graphql/static_validation/base_visitor.rb +++ b/lib/graphql/static_validation/base_visitor.rb @@ -10,6 +10,7 @@ def initialize(document, context) @argument_definitions = [] @directive_definitions = [] @context = context + @types = context.query.types @schema = context.schema super(document) end @@ -77,7 +78,7 @@ def on_inline_fragment(node, parent) def on_field(node, parent) parent_type = @object_types.last - field_definition = @schema.get_field(parent_type, node.name, @context.query.context) + field_definition = @types.field(parent_type, node.name) @field_definitions.push(field_definition) if !field_definition.nil? next_object_type = field_definition.type.unwrap @@ -103,14 +104,14 @@ def on_argument(node, parent) argument_defn = if (arg = @argument_definitions.last) arg_type = arg.type.unwrap if arg_type.kind.input_object? - @context.warden.get_argument(arg_type, node.name) + @types.argument(arg_type, node.name) else nil end elsif (directive_defn = @directive_definitions.last) - @context.warden.get_argument(directive_defn, node.name) + @types.argument(directive_defn, node.name) elsif (field_defn = @field_definitions.last) - @context.warden.get_argument(field_defn, node.name) + @types.argument(field_defn, node.name) else nil end @@ -170,7 +171,7 @@ def argument_definition def on_fragment_with_type(node) object_type = if node.type - @context.warden.get_type(node.type.name) + @types.type(node.type.name) else @object_types.last end diff --git a/lib/graphql/static_validation/literal_validator.rb b/lib/graphql/static_validation/literal_validator.rb index 4e1f5b64ff..5b2ec5f090 100644 --- a/lib/graphql/static_validation/literal_validator.rb +++ b/lib/graphql/static_validation/literal_validator.rb @@ -5,7 +5,7 @@ module StaticValidation class LiteralValidator def initialize(context:) @context = context - @warden = context.warden + @types = context.types @invalid_response = GraphQL::Query::InputValidationResult.new(valid: false, problems: []) @valid_response = GraphQL::Query::InputValidationResult.new(valid: true, problems: []) end @@ -109,7 +109,7 @@ def constant_scalar?(ast_value) def required_input_fields_are_present(type, ast_node) # TODO - would be nice to use these to create an error message so the caller knows # that required fields are missing - required_field_names = @warden.arguments(type) + required_field_names = @types.arguments(type) .select { |argument| argument.type.kind.non_null? && !argument.default_value? } .map!(&:name) @@ -119,7 +119,7 @@ def required_input_fields_are_present(type, ast_node) missing_required_field_names.empty? ? @valid_response : @invalid_response else results = missing_required_field_names.map do |name| - arg_type = @warden.get_argument(type, name).type + arg_type = @types.argument(type, name).type recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type) end if type.one_of? && ast_node.arguments.size != 1 @@ -131,7 +131,7 @@ def required_input_fields_are_present(type, ast_node) def present_input_field_values_are_valid(type, ast_node) results = ast_node.arguments.map do |value| - field = @warden.get_argument(type, value.name) + field = @types.argument(type, value.name) # we want to call validate on an argument even if it's an invalid one # so that our raise exception is on it instead of the entire InputObject field_type = field && field.type diff --git a/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb b/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb index 92c467d411..f167885fd0 100644 --- a/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +++ b/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb @@ -15,7 +15,7 @@ def on_argument(node, parent) if @context.schema.error_bubbling || context.errors.none? { |err| err.path.take(@path.size) == @path } parent_defn = parent_definition(parent) - if parent_defn && (arg_defn = context.warden.get_argument(parent_defn, node.name)) + if parent_defn && (arg_defn = @types.argument(parent_defn, node.name)) validation_result = context.validate_literal(node.value, arg_defn.type) if !validation_result.valid? kind_of_node = node_type(parent) diff --git a/lib/graphql/static_validation/rules/arguments_are_defined.rb b/lib/graphql/static_validation/rules/arguments_are_defined.rb index 699086a3c9..57f6e55a55 100644 --- a/lib/graphql/static_validation/rules/arguments_are_defined.rb +++ b/lib/graphql/static_validation/rules/arguments_are_defined.rb @@ -5,7 +5,7 @@ module ArgumentsAreDefined def on_argument(node, parent) parent_defn = parent_definition(parent) - if parent_defn && context.warden.get_argument(parent_defn, node.name) + if parent_defn && @types.argument(parent_defn, node.name) super elsif parent_defn kind_of_node = node_type(parent) diff --git a/lib/graphql/static_validation/rules/directives_are_defined.rb b/lib/graphql/static_validation/rules/directives_are_defined.rb index 90bb27aa5d..4c91df738c 100644 --- a/lib/graphql/static_validation/rules/directives_are_defined.rb +++ b/lib/graphql/static_validation/rules/directives_are_defined.rb @@ -4,11 +4,10 @@ module StaticValidation module DirectivesAreDefined def initialize(*) super - @directive_names = context.warden.directives.map(&:graphql_name) end def on_directive(node, parent) - if !@directive_names.include?(node.name) + if !@types.directive_exists?(node.name) @directives_are_defined_errors_by_name ||= {} error = @directives_are_defined_errors_by_name[node.name] ||= begin err = GraphQL::StaticValidation::DirectivesAreDefinedError.new( diff --git a/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb b/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb index 51576ee6b9..40188ecef7 100644 --- a/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +++ b/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb @@ -4,7 +4,7 @@ module StaticValidation module FieldsAreDefinedOnType def on_field(node, parent) parent_type = @object_types[-2] - field = context.warden.get_field(parent_type, node.name) + field = context.query.types.field(parent_type, node.name) if field.nil? if parent_type.kind.union? diff --git a/lib/graphql/static_validation/rules/fields_will_merge.rb b/lib/graphql/static_validation/rules/fields_will_merge.rb index 9529914680..d4aaff5ba1 100644 --- a/lib/graphql/static_validation/rules/fields_will_merge.rb +++ b/lib/graphql/static_validation/rules/fields_will_merge.rb @@ -117,8 +117,8 @@ def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutuall return if fragment1.nil? || fragment2.nil? - fragment_type1 = context.warden.get_type(fragment1.type.name) - fragment_type2 = context.warden.get_type(fragment2.type.name) + fragment_type1 = context.query.types.type(fragment1.type.name) + fragment_type2 = context.query.types.type(fragment2.type.name) return if fragment_type1.nil? || fragment_type2.nil? @@ -170,7 +170,7 @@ def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually fragment = context.fragments[fragment_name] return if fragment.nil? - fragment_type = context.warden.get_type(fragment.type.name) + fragment_type = @types.type(fragment.type.name) return if fragment_type.nil? fragment_fields, fragment_spreads = fields_and_fragments_from_selection(fragment, owner_type: fragment_type, parents: [*fragment_spread.parents, fragment_type]) @@ -340,10 +340,10 @@ def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragme selections.each do |node| case node when GraphQL::Language::Nodes::Field - definition = context.warden.get_field(owner_type, node.name) + definition = @types.field(owner_type, node.name) fields << Field.new(node, definition, owner_type, parents) when GraphQL::Language::Nodes::InlineFragment - fragment_type = node.type ? context.warden.get_type(node.type.name) : owner_type + fragment_type = node.type ? @types.type(node.type.name) : owner_type find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: owner_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type when GraphQL::Language::Nodes::FragmentSpread fragment_spreads << FragmentSpread.new(node.name, parents) @@ -411,8 +411,8 @@ def mutually_exclusive?(parents1, parents2) false else # Check if these two scopes have _any_ types in common. - possible_right_types = context.query.possible_types(type1) - possible_left_types = context.query.possible_types(type2) + possible_right_types = context.types.possible_types(type1) + possible_left_types = context.types.possible_types(type2) (possible_right_types & possible_left_types).empty? end end diff --git a/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb b/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb index a800a481d3..f7e045f59e 100644 --- a/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +++ b/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb @@ -28,7 +28,7 @@ def on_document(node, parent) frag_node = context.fragments[frag_spread.node.name] if frag_node fragment_child_name = frag_node.type.name - fragment_child = context.warden.get_type(fragment_child_name) + fragment_child = @types.type(fragment_child_name) # Might be non-existent type name if fragment_child validate_fragment_in_scope(frag_spread.parent_type, fragment_child, frag_spread.node, context, frag_spread.path) @@ -44,8 +44,8 @@ def validate_fragment_in_scope(parent_type, child_type, node, context, path) # It's not a valid fragment type, this error was handled someplace else return end - parent_types = context.warden.possible_types(parent_type.unwrap) - child_types = context.warden.possible_types(child_type.unwrap) + parent_types = @types.possible_types(parent_type.unwrap) + child_types = @types.possible_types(child_type.unwrap) if child_types.none? { |c| parent_types.include?(c) } name = node.respond_to?(:name) ? " #{node.name}" : "" diff --git a/lib/graphql/static_validation/rules/fragment_types_exist.rb b/lib/graphql/static_validation/rules/fragment_types_exist.rb index 9863eaa968..07c8e533b4 100644 --- a/lib/graphql/static_validation/rules/fragment_types_exist.rb +++ b/lib/graphql/static_validation/rules/fragment_types_exist.rb @@ -21,7 +21,7 @@ def validate_type_exists(fragment_node) true else type_name = fragment_node.type.name - type = context.warden.get_type(type_name) + type = @types.type(type_name) if type.nil? add_error(GraphQL::StaticValidation::FragmentTypesExistError.new( "No such type #{type_name}, so it can't be a fragment condition", diff --git a/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb b/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb index c97b6f8284..63f4aa49e8 100644 --- a/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +++ b/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb @@ -19,7 +19,7 @@ def validate_type_is_composite(node) true else type_name = node_type.to_query_string - type_def = context.warden.get_type(type_name) + type_def = @types.type(type_name) if type_def.nil? || !type_def.kind.composite? add_error(GraphQL::StaticValidation::FragmentsAreOnCompositeTypesError.new( "Invalid fragment on type #{type_name} (must be Union, Interface or Object)", diff --git a/lib/graphql/static_validation/rules/mutation_root_exists.rb b/lib/graphql/static_validation/rules/mutation_root_exists.rb index 8a0929b687..903221c851 100644 --- a/lib/graphql/static_validation/rules/mutation_root_exists.rb +++ b/lib/graphql/static_validation/rules/mutation_root_exists.rb @@ -3,7 +3,7 @@ module GraphQL module StaticValidation module MutationRootExists def on_operation_definition(node, _parent) - if node.operation_type == 'mutation' && context.warden.root_type_for_operation("mutation").nil? + if node.operation_type == 'mutation' && context.query.types.mutation_root.nil? add_error(GraphQL::StaticValidation::MutationRootExistsError.new( 'Schema is not configured for mutations', nodes: node diff --git a/lib/graphql/static_validation/rules/query_root_exists.rb b/lib/graphql/static_validation/rules/query_root_exists.rb index e9b11170c4..a27207dbef 100644 --- a/lib/graphql/static_validation/rules/query_root_exists.rb +++ b/lib/graphql/static_validation/rules/query_root_exists.rb @@ -3,7 +3,7 @@ module GraphQL module StaticValidation module QueryRootExists def on_operation_definition(node, _parent) - if (node.operation_type == 'query' || node.operation_type.nil?) && context.warden.root_type_for_operation("query").nil? + if (node.operation_type == 'query' || node.operation_type.nil?) && context.query.types.query_root.nil? add_error(GraphQL::StaticValidation::QueryRootExistsError.new( 'Schema is not configured for queries', nodes: node diff --git a/lib/graphql/static_validation/rules/required_arguments_are_present.rb b/lib/graphql/static_validation/rules/required_arguments_are_present.rb index a3eb58e03d..3140828447 100644 --- a/lib/graphql/static_validation/rules/required_arguments_are_present.rb +++ b/lib/graphql/static_validation/rules/required_arguments_are_present.rb @@ -16,11 +16,11 @@ def on_directive(node, _parent) private def assert_required_args(ast_node, defn) - args = defn.arguments(context.query.context) + args = @context.query.types.arguments(defn) return if args.empty? present_argument_names = ast_node.arguments.map(&:name) - required_argument_names = context.warden.arguments(defn) - .select { |a| a.type.kind.non_null? && !a.default_value? && context.warden.get_argument(defn, a.name) } + required_argument_names = context.query.types.arguments(defn) + .select { |a| a.type.kind.non_null? && !a.default_value? && context.query.types.argument(defn, a.name) } .map!(&:name) missing_names = required_argument_names - present_argument_names diff --git a/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb b/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb index bdbe0a1965..08461f4930 100644 --- a/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +++ b/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb @@ -26,7 +26,7 @@ def get_parent_type(context, parent) context.directive_definition || context.field_definition end - parent_type = context.warden.get_argument(defn, parent_name(parent, defn)) + parent_type = context.types.argument(defn, parent_name(parent, defn)) parent_type ? parent_type.type.unwrap : nil end @@ -34,7 +34,7 @@ def validate_input_object(ast_node, context, parent) parent_type = get_parent_type(context, parent) return unless parent_type && parent_type.kind.input_object? - required_fields = context.warden.arguments(parent_type) + required_fields = context.types.arguments(parent_type) .select{ |arg| arg.type.kind.non_null? && !arg.default_value? } .map!(&:graphql_name) @@ -43,7 +43,7 @@ def validate_input_object(ast_node, context, parent) missing_fields.each do |missing_field| path = [*context.path, missing_field] - missing_field_type = context.warden.get_argument(parent_type, missing_field).type + missing_field_type = context.types.argument(parent_type, missing_field).type add_error(RequiredInputObjectAttributesArePresentError.new( "Argument '#{missing_field}' on InputObject '#{parent_type.to_type_signature}' is required. Expected type #{missing_field_type.to_type_signature}", argument_name: missing_field, diff --git a/lib/graphql/static_validation/rules/subscription_root_exists.rb b/lib/graphql/static_validation/rules/subscription_root_exists.rb index b4ccf10f75..b3c41748f1 100644 --- a/lib/graphql/static_validation/rules/subscription_root_exists.rb +++ b/lib/graphql/static_validation/rules/subscription_root_exists.rb @@ -3,7 +3,7 @@ module GraphQL module StaticValidation module SubscriptionRootExists def on_operation_definition(node, _parent) - if node.operation_type == "subscription" && context.warden.root_type_for_operation("subscription").nil? + if node.operation_type == "subscription" && context.types.subscription_root.nil? add_error(GraphQL::StaticValidation::SubscriptionRootExistsError.new( 'Schema is not configured for subscriptions', nodes: node diff --git a/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb b/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb index ebafb674c9..eb8376cad5 100644 --- a/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +++ b/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb @@ -65,7 +65,7 @@ def validate_usage(argument_owner, arg_node, ast_var) end end - arg_defn = context.warden.get_argument(argument_owner, arg_node.name) + arg_defn = @types.argument(argument_owner, arg_node.name) arg_defn_type = arg_defn.type # If the argument is non-null, but it was given a default value, diff --git a/lib/graphql/static_validation/rules/variables_are_input_types.rb b/lib/graphql/static_validation/rules/variables_are_input_types.rb index 45cee45a63..29c719333d 100644 --- a/lib/graphql/static_validation/rules/variables_are_input_types.rb +++ b/lib/graphql/static_validation/rules/variables_are_input_types.rb @@ -4,7 +4,7 @@ module StaticValidation module VariablesAreInputTypes def on_variable_definition(node, parent) type_name = get_type_name(node.type) - type = context.warden.get_type(type_name) + type = context.query.types.type(type_name) if type.nil? add_error(GraphQL::StaticValidation::VariablesAreInputTypesError.new( diff --git a/lib/graphql/static_validation/validation_context.rb b/lib/graphql/static_validation/validation_context.rb index 9d08734c41..440715283f 100644 --- a/lib/graphql/static_validation/validation_context.rb +++ b/lib/graphql/static_validation/validation_context.rb @@ -13,14 +13,14 @@ class ValidationContext attr_reader :query, :errors, :visitor, :on_dependency_resolve_handlers, - :max_errors, :warden, :schema + :max_errors, :types, :schema def_delegators :@query, :document, :fragments, :operations def initialize(query, visitor_class, max_errors) @query = query - @warden = query.warden + @types = query.types # TODO update migrated callers to use this accessor @schema = query.schema @literal_validator = LiteralValidator.new(context: query.context) @errors = [] diff --git a/lib/graphql/subscriptions.rb b/lib/graphql/subscriptions.rb index 450751c759..b87dfe2ea3 100644 --- a/lib/graphql/subscriptions.rb +++ b/lib/graphql/subscriptions.rb @@ -64,12 +64,12 @@ def trigger(event_name, args, object, scope: nil, context: {}) event_name = event_name.to_s # Try with the verbatim input first: - field = @schema.get_field(@schema.subscription, event_name, context) + field = dummy_query.types.field(@schema.subscription, event_name) # rubocop:disable Development/ContextIsPassedCop if field.nil? # And if it wasn't found, normalize it: normalized_event_name = normalize_name(event_name) - field = @schema.get_field(@schema.subscription, normalized_event_name, context) + field = dummy_query.types.field(@schema.subscription, normalized_event_name) # rubocop:disable Development/ContextIsPassedCop if field.nil? raise InvalidTriggerError, "No subscription matching trigger: #{event_name} (looked for #{@schema.subscription.graphql_name}.#{normalized_event_name})" end diff --git a/lib/graphql/subscriptions/event.rb b/lib/graphql/subscriptions/event.rb index ff79f6d0be..a5f7127a1a 100644 --- a/lib/graphql/subscriptions/event.rb +++ b/lib/graphql/subscriptions/event.rb @@ -137,7 +137,7 @@ def stringify_args(arg_owner, args, context) end def get_arg_definition(arg_owner, arg_name, context) - arg_owner.get_argument(arg_name, context) || arg_owner.arguments(context).each_value.find { |v| v.keyword.to_s == arg_name } + context.types.argument(arg_owner, arg_name) || context.types.arguments(arg_owner).find { |v| v.keyword.to_s == arg_name } end end end diff --git a/lib/graphql/testing/helpers.rb b/lib/graphql/testing/helpers.rb index ab044d20ee..f6f700e2ce 100644 --- a/lib/graphql/testing/helpers.rb +++ b/lib/graphql/testing/helpers.rb @@ -43,7 +43,7 @@ def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, as type_name, *field_names = field_path.split(".") dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context) query_context = dummy_query.context - object_type = dummy_query.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop + object_type = dummy_query.types.type(type_name) # rubocop:disable Development/ContextIsPassedCop if object_type graphql_result = object field_names.each do |field_name| @@ -52,7 +52,7 @@ def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, as if graphql_result.nil? return nil end - visible_field = dummy_query.get_field(object_type, field_name) + visible_field = dummy_query.types.field(object_type, field_name) # rubocop:disable Development/ContextIsPassedCop if visible_field dummy_query.context.dataloader.run_isolated { field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context) diff --git a/spec/graphql/logger_spec.rb b/spec/graphql/logger_spec.rb index 9ec6a9fee7..d5605cf4c0 100644 --- a/spec/graphql/logger_spec.rb +++ b/spec/graphql/logger_spec.rb @@ -98,7 +98,12 @@ class CustomLoggerSchema < DefaultLoggerSchema it "logs about hidden interfaces with no implementations" do res = LoggerTest::CustomLoggerSchema.execute("{ node(id: \"5\") { id } }") assert_equal ["Field 'node' doesn't exist on type 'Query'"], res["errors"].map { |err| err["message"] } - assert_includes LoggerTest::CustomLoggerSchema::LOG_STRING.string, "Interface `Node` hidden because it has no visible implementers" + if res.query.types.is_a?(GraphQL::Schema::Subset) + # TODO make this actually test something? + assert_equal "", LoggerTest::CustomLoggerSchema::LOG_STRING.string + else + assert_includes LoggerTest::CustomLoggerSchema::LOG_STRING.string, "Interface `Node` hidden because it has no visible implementers" + end end it "doesn't print messages by default" do diff --git a/spec/graphql/schema/build_from_definition_spec.rb b/spec/graphql/schema/build_from_definition_spec.rb index f99fe40973..769d7272dd 100644 --- a/spec/graphql/schema/build_from_definition_spec.rb +++ b/spec/graphql/schema/build_from_definition_spec.rb @@ -137,7 +137,14 @@ def assert_schema_and_compare_output(definition) secret_type = parsed_schema.get_type("Secret") assert_equal 2, secret_type.directives.size - assert_schema_and_compare_output(schema) + if GraphQL::Schema.use_schema_subset? + # Subset hides this because it has no possible types of its own :S + schema_output = GraphQL::Schema.from_definition(schema).to_definition + expected_output = schema.sub(/interface Secret2[^}]+\}\n\n/m, "") + assert_equal expected_output, schema_output + else + assert_schema_and_compare_output(schema) + end end it 'supports descriptions and definition_line' do diff --git a/spec/graphql/schema/dynamic_members_spec.rb b/spec/graphql/schema/dynamic_members_spec.rb index 10b20ec30d..32a0bd8191 100644 --- a/spec/graphql/schema/dynamic_members_spec.rb +++ b/spec/graphql/schema/dynamic_members_spec.rb @@ -474,7 +474,8 @@ def legacy_schema_sdl assert_equal "String", exec_future_query(introspection_query_str)["data"]["__type"]["fields"].find { |f| f["name"] == "f1" }["type"]["name"] # Schema dump - assert_includes legacy_schema_sdl, <<-GRAPHQL + legacy_query_type_str = legacy_schema_sdl[/type Query \{[^}]*\}/m] + assert_equal legacy_query_type_str, <<-GRAPHQL.chomp type Query { actor: Actor add(left: Int!, right: Int!): String! diff --git a/spec/graphql/schema/enum_spec.rb b/spec/graphql/schema/enum_spec.rb index 9d214bb4df..8d8754fdf2 100644 --- a/spec/graphql/schema/enum_spec.rb +++ b/spec/graphql/schema/enum_spec.rb @@ -206,7 +206,7 @@ def empty_enum assert_equal("YAK", enum.coerce_isolated_result("YAK")) # NOT OK assert_raises(GraphQL::Schema::Enum::UnresolvedValueError) { - enum.coerce_result("YAK", OpenStruct.new(warden: NothingWarden)) + enum.coerce_result("YAK", OpenStruct.new(types: NothingWarden)) } end end diff --git a/spec/graphql/schema/resolver_spec.rb b/spec/graphql/schema/resolver_spec.rb index f34c722602..6b709fce1e 100644 --- a/spec/graphql/schema/resolver_spec.rb +++ b/spec/graphql/schema/resolver_spec.rb @@ -543,7 +543,7 @@ def exec_query(*args, **kwargs) end it "works on instances" do - r = ResolverTest::Resolver1.new(object: nil, context: nil, field: nil) + r = ResolverTest::Resolver1.new(object: nil, context: GraphQL::Query::NullContext.instance, field: nil) assert_equal "Resolver1", r.path end end diff --git a/spec/graphql/schema/subset_spec.rb b/spec/graphql/schema/subset_spec.rb new file mode 100644 index 0000000000..198b7c8ec7 --- /dev/null +++ b/spec/graphql/schema/subset_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "spec_helper" + +describe GraphQL::Schema::Subset do + class SubsetSchema < GraphQL::Schema + class Thing < GraphQL::Schema::Object + field :name, String + end + + class Query < GraphQL::Schema::Object + field :thing, Thing, fallback_value: :Something + field :greeting, String + end + + query(Query) + end + it "only loads the types it needs" do + skip "TODO optimize how this thing works" + query = GraphQL::Query.new(SubsetSchema, "{ thing { name } }", use_schema_subset: true) + assert_equal [], query.types.loaded_types + res = query.result + + assert_equal "Something", res["data"]["thing"]["name"] + assert_equal ["Query", "String", "Thing"], query.types.loaded_types.map(&:graphql_name).sort + end +end diff --git a/spec/graphql/schema/type_expression_spec.rb b/spec/graphql/schema/type_expression_spec.rb index 2541bed606..726a64bef1 100644 --- a/spec/graphql/schema/type_expression_spec.rb +++ b/spec/graphql/schema/type_expression_spec.rb @@ -2,12 +2,11 @@ require "spec_helper" describe GraphQL::Schema::TypeExpression do - let(:type_owner) { Dummy::Schema } let(:ast_node) { document = GraphQL.parse("query dostuff($var: #{type_name}) { id } ") document.definitions.first.variables.first.type } - let(:type_expression_result) { GraphQL::Schema::TypeExpression.build_type(type_owner, ast_node) } + let(:type_expression_result) { Dummy::Schema.type_from_ast(ast_node) } describe "#type" do describe "simple types" do diff --git a/spec/graphql/schema/warden_spec.rb b/spec/graphql/schema/warden_spec.rb index 713f311f0f..0195be564a 100644 --- a/spec/graphql/schema/warden_spec.rb +++ b/spec/graphql/schema/warden_spec.rb @@ -167,7 +167,6 @@ class PublicType < BaseObject end class CheremeWithInterface < BaseObject - # Commenting this would make the test pass implements PublicInterfaceType field :name, String, null: false @@ -224,8 +223,7 @@ class QueryType < BaseObject field :manners, [MannerType], null: false - # Commenting this would make the test pass - field :test, PublicType, null: false + field :public_type, PublicType, null: false end class MutationType < BaseObject diff --git a/spec/graphql/subscriptions/event_spec.rb b/spec/graphql/subscriptions/event_spec.rb index 9aea7411e8..67256e1d95 100644 --- a/spec/graphql/subscriptions/event_spec.rb +++ b/spec/graphql/subscriptions/event_spec.rb @@ -21,15 +21,20 @@ class Subscription < GraphQL::Schema::Object subscription(Subscription) end + def build_dummy_context(context = {}) + GraphQL::Query.new(EventSchema, "{ __typename }", context: context).context + end + + it "should serialize a JSON argument into the topic name" do field = EventSchema.subscription.fields["jsonSubscription"] - event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "b" => 1, "a" => 0 } }, field: field, context: nil, scope: nil) + event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "b" => 1, "a" => 0 } }, field: field, context: build_dummy_context, scope: nil) assert_equal %Q{:jsonSubscription:someJson:{"a":0,"b":1}}, event.topic end it "should not serialize the context into the topic name" do field = EventSchema.subscription.fields["jsonSubscription"] - context = { my_id: "abc" } + context = build_dummy_context({ my_id: "abc" }) event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "b" => 1, "a" => 0 } }, field: field, context: context, scope: nil) assert_equal %Q{:jsonSubscription:someJson:{"a":0,"b":1}}, event.topic assert_equal event.context[:my_id], "abc" @@ -37,8 +42,8 @@ class Subscription < GraphQL::Schema::Object it "should serialize two equivalent JSON hashes with different key orderings into equivalent topic names" do field = EventSchema.subscription.fields["jsonSubscription"] - event_a = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "b" => 1, "a" => 0 } }, field: field, context: nil, scope: nil) - event_b = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "a" => 0, "b" => 1 } }, field: field, context: nil, scope: nil) + event_a = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "b" => 1, "a" => 0 } }, field: field, context: build_dummy_context, scope: nil) + event_b = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "a" => 0, "b" => 1 } }, field: field, context: build_dummy_context, scope: nil) assert_equal event_a.topic, event_b.topic end @@ -52,25 +57,25 @@ class Subscription < GraphQL::Schema::Object }, "a" => 0 } - event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => nested_hash }, field: field, context: nil, scope: nil) + event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => nested_hash }, field: field, context: build_dummy_context, scope: nil) assert_equal %Q{:jsonSubscription:someJson:{"a":0,"b":1,"c":{"y":99,"z":100}}}, event.topic end it "should serialize a hash inside an array as a sorted hash" do field = EventSchema.subscription.fields["jsonSubscription"] - event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => [{ "b" => 1, "a" => 0 }] }, field: field, context: nil, scope: nil) + event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => [{ "b" => 1, "a" => 0 }] }, field: field, context: build_dummy_context, scope: nil) assert_equal %Q{:jsonSubscription:someJson:[{"a":0,"b":1}]}, event.topic end it "should serialize a hash inside an array of an array as a sorted hash" do field = EventSchema.subscription.fields["jsonSubscription"] - event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => [[{ "b" => 1, "a" => 0 }]] }, field: field, context: nil, scope: nil) + event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => [[{ "b" => 1, "a" => 0 }]] }, field: field, context: build_dummy_context, scope: nil) assert_equal %Q{:jsonSubscription:someJson:[[{"a":0,"b":1}]]}, event.topic end it "should serialize a hash inside an array inside of a hash" do field = EventSchema.subscription.fields["jsonSubscription"] - event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "key" => [{ "b" => 1, "a" => 0}]} }, field: field, context: nil, scope: nil) + event = GraphQL::Subscriptions::Event.new(name: "test", arguments: { "someJson" => { "key" => [{ "b" => 1, "a" => 0}]} }, field: field, context: build_dummy_context, scope: nil) assert_equal %Q{:jsonSubscription:someJson:{"key":[{"a":0,"b":1}]}}, event.topic end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 53db1d6d60..11f35625a5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,6 +19,8 @@ if ENV["GRAPHQL_REJECT_NUMBERS_FOLLOWED_BY_NAMES"] puts "Opting into GraphQL.reject_numbers_followed_by_names" GraphQL.reject_numbers_followed_by_names = true + puts "Opting into GraphQL::Schema::Subset" + GraphQL::Schema.use_schema_subset = true end require "rake"