Skip to content

Field Filters #1411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/graphql/relay/connection_instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module ConnectionInstrumentation
# - Merging in the default arguments
# - Transforming its resolve function to return a connection object
def self.instrument(type, field)
if field.connection?
if field.connection? && !field.metadata[:field_instance]
connection_arguments = DEFAULT_ARGUMENTS.merge(field.arguments)
original_resolve = field.resolve_proc
original_lazy_resolve = field.lazy_resolve_proc
Expand Down
3 changes: 1 addition & 2 deletions lib/graphql/relay/connection_resolve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ def call(obj, args, ctx)

def build_connection(nodes, args, parent, ctx)
if nodes.is_a? GraphQL::ExecutionError
ctx.add_error(nodes)
nil
raise nodes
else
if parent.is_a?(GraphQL::Schema::Object)
parent = parent.object
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/relay/mutation/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module Instrumentation
# By using an instrumention, we can apply our wrapper _last_,
# giving users access to the original resolve function in earlier instrumentation.
def self.instrument(type, field)
if field.mutation.is_a?(GraphQL::Relay::Mutation) || (field.mutation.is_a?(Class) && field.mutation < GraphQL::Schema::RelayClassicMutation)
if field.mutation.is_a?(GraphQL::Relay::Mutation)
new_resolve = Mutation::Resolve.new(field.mutation, field.resolve_proc)
new_lazy_resolve = Mutation::Resolve.new(field.mutation, field.lazy_resolve_proc)
field.redefine(resolve: new_resolve, lazy_resolve: new_lazy_resolve)
Expand Down
2 changes: 2 additions & 0 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
require "graphql/schema/enum_value"
require "graphql/schema/enum"
require "graphql/schema/field"
require "graphql/schema/filter"
require "graphql/schema/input_object"
require "graphql/schema/interface"
require "graphql/schema/mutation"
Expand Down Expand Up @@ -154,6 +155,7 @@ def initialize
@instrumenters = Hash.new { |h, k| h[k] = [] }
@lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
@lazy_methods.set(GraphQL::Relay::ConnectionResolve::LazyNodesWrapper, :never_called)
@lazy_methods.set(GraphQL::Schema::Filter::LazyThingy, :value)
@cursor_encoder = Base64Encoder
# Default to the built-in execution strategy:
@query_execution_strategy = self.class.default_execution_strategy
Expand Down
174 changes: 132 additions & 42 deletions lib/graphql/schema/field.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true
# test_via: ../object.rb
require "graphql/schema/field/dynamic_resolve"
require "graphql/schema/field/unwrapped_resolve"
module GraphQL
class Schema
class Field
Expand All @@ -15,12 +13,18 @@ class Field
# @return [String]
attr_accessor :description

# @return [Symbol]
attr_reader :method
# @return [Symbol] Method or hash key to look up
attr_reader :method_sym

# @return [String] Method or hash key to look up
attr_reader :method_str

# @return [Class] The type that this field belongs to
attr_reader :owner

# @return [Integer, nil] The max page size for connections, if configured
attr_reader :max_page_size

# @return [Class, nil] The mutation this field was derived from, if there is one
def mutation
@mutation || @mutation_class
Expand All @@ -46,7 +50,8 @@ def mutation
# @param arguments [{String=>GraphQL::Schema::Arguments}] Arguments for this field (may be added in the block, also)
# @param camelize [Boolean] If true, the field name will be camelized when building the schema
# @param complexity [Numeric] When provided, set the complexity for this field
def initialize(name, return_type_expr = nil, desc = nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, connection: nil, max_page_size: nil, resolve: nil, introspection: false, hash_key: nil, camelize: true, complexity: 1, extras: [], mutation: nil, mutation_class: nil, arguments: {}, &definition_block)
# @param filters [Array<Class>]
def initialize(name, return_type_expr = nil, desc = nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, connection: nil, max_page_size: nil, resolve: nil, introspection: false, hash_key: nil, camelize: true, complexity: 1, extras: [], mutation: nil, mutation_class: nil, arguments: {}, filters: [], &definition_block)
if (field || function) && desc.nil? && return_type_expr.is_a?(String)
# The return type should be copied from `field` or `function`, and the second positional argument is the description
desc = return_type_expr
Expand Down Expand Up @@ -82,8 +87,12 @@ def initialize(name, return_type_expr = nil, desc = nil, owner: nil, null: nil,
if method && hash_key
raise ArgumentError, "Provide `method:` _or_ `hash_key:`, not both. (called with: `method: #{method.inspect}, hash_key: #{hash_key.inspect}`)"
end
@method = method
@hash_key = hash_key

# TODO: I think non-string/symbol hash keys are wrongly normalized (eg `1` will not work)
method_name = method || hash_key || Member::BuildType.underscore(name.to_s)

@method_str = method_name.to_s
@method_sym = method_name.to_sym
@complexity = complexity
@return_type_expr = return_type_expr
@return_type_null = null
Expand All @@ -101,8 +110,13 @@ def initialize(name, return_type_expr = nil, desc = nil, owner: nil, null: nil,
if definition_block
instance_eval(&definition_block)
end

@filters = filters.map { |f| f.new(field: self) }
end

# @return [Array<GraphQL::Schema::Filter>]
attr_reader :filters

def description(text = nil)
if text
@description = text
Expand Down Expand Up @@ -140,7 +154,6 @@ def to_graphql
return field_inst.to_graphql
end

method_name = @method || @hash_key || Member::BuildType.underscore(@name)

field_defn = if @field
@field.dup
Expand All @@ -152,20 +165,19 @@ def to_graphql

field_defn.name = @camelize ? Member::BuildType.camelize(name) : name
if @return_type_expr
return_type_name = Member::BuildType.to_type_name(@return_type_expr)
connection = @connection.nil? ? return_type_name.end_with?("Connection") : @connection
field_defn.type = -> {
begin
Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
rescue
raise ArgumentError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: #{$!.message}", $!.backtrace
end
}
elsif @connection.nil? && (@field || @function)
return_type_name = Member::BuildType.to_type_name(field_defn.type)
connection = return_type_name.end_with?("Connection")
else
connection = @connection
field_defn.type = -> { type }
end

if @connection.nil?
# Provide default based on type name
return_type_name = if @field || @function
Member::BuildType.to_type_name(field_defn.type)
elsif @return_type_expr
Member::BuildType.to_type_name(@return_type_expr)
else
raise "No connection info possible"
end
@connection = return_type_name.end_with?("Connection")
end

if @description
Expand All @@ -180,40 +192,118 @@ def to_graphql
field_defn.mutation = @mutation_class
end

field_defn.resolve = if @resolve || @function || @field
prev_resolve = @resolve || field_defn.resolve_proc
UnwrappedResolve.new(inner_resolve: prev_resolve)
else
DynamicResolve.new(
method_name: method_name,
connection: connection,
extras: @extras
)
end

field_defn.connection = connection
field_defn.resolve = self.method(:resolve_field)
field_defn.connection = @connection
field_defn.connection_max_page_size = @max_page_size
field_defn.introspection = @introspection
field_defn.complexity = @complexity

# apply this first, so it can be overriden below
if connection
# TODO: this could be a bit weird, because these fields won't be present
# after initialization, only in the `to_graphql` response.
# This calculation _could_ be moved up if need be.
argument :after, "String", "Returns the elements in the list that come after the specified global ID.", required: false
argument :before, "String", "Returns the elements in the list that come before the specified global ID.", required: false
argument :first, "Int", "Returns the first _n_ elements from the list.", required: false
argument :last, "Int", "Returns the last _n_ elements from the list.", required: false
if @connection
@filters.unshift(Schema::ConnectionFilter.new(field: self))
end

arguments.each do |name, defn|
arg_graphql = defn.to_graphql
field_defn.arguments[arg_graphql.name] = arg_graphql
end

field_defn.metadata[:field_instance] = self

field_defn
end

def type
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
rescue
raise ArgumentError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: #{$!.message}", $!.backtrace
end

# Implement {GraphQL::Field}'s resolve API.
#
# Eventually, we might hook up field instances to execution in another way. TBD.
def resolve_field(obj, args, ctx)
if args.any? || @extras.any?
# Splat the GraphQL::Arguments to Ruby keyword arguments
ruby_kwargs = args.to_kwargs
@extras.each do |extra_arg|
# TODO: provide proper tests for `:ast_node`, `:irep_node`, `:parent`, others?
ruby_kwargs[extra_arg] = ctx.public_send(extra_arg)
end
else
ruby_kwargs = NO_ARGS
end

with_filters(obj, ruby_kwargs) do |filtered_obj, filtered_ruby_kwargs|
if @resolve || @function || @field
# Support a passed-in proc, one way or another
prev_resolve = if @resolve
@resolve
elsif @function
@function
elsif @field
@field.resolve_proc
end

# Might be nil, still want to call the func in that case
inner_obj = filtered_obj && filtered_obj.object
prev_resolve.call(inner_obj, args, ctx)
elsif @mutation_class
mutation_inst = @mutation_class.new(object: filtered_obj, arguments: args, context: ctx.query.context)
mutation_inst.resolve(**filtered_ruby_kwargs)
else
resolve_field_dynamic(filtered_obj, filtered_ruby_kwargs)
end
end
end

private

def with_filters(obj, args, filter_idx: 0)
next_filter = @filters[filter_idx]
if next_filter
next_filter.resolve_field(obj, args) do |obj2, args2|
with_filters(obj2, args2, filter_idx: filter_idx + 1) do |obj3, args3|
yield(obj3, args3)
end
end
else
yield(obj, args)
end
end

# Try a few ways to resolve the field
# @api private
def resolve_field_dynamic(obj, ruby_kwargs)
if obj.respond_to?(@method_sym)
public_send_field(obj, @method_sym, ruby_kwargs)
elsif obj.object.is_a?(Hash)
inner_object = obj.object
inner_object[@method_sym] || inner_object[@method_str]
elsif obj.object.respond_to?(@method_sym)
public_send_field(obj.object, @method_sym, ruby_kwargs)
else
raise <<-ERR
Failed to implement #{@owner.name}.#{name}, tried:

- `#{obj.class}##{@method_sym}`, which did not exist
- `#{obj.object.class}##{@method_sym}`, which did not exist
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash

To implement this field, define one of the methods above (and check for typos)
ERR
end
end

NO_ARGS = {}.freeze

def public_send_field(obj, method_name, ruby_kwargs)
if ruby_kwargs.any?
obj.public_send(method_name, **ruby_kwargs)
else
obj.public_send(method_name)
end
end
end
end
end
70 changes: 0 additions & 70 deletions lib/graphql/schema/field/dynamic_resolve.rb

This file was deleted.

20 changes: 0 additions & 20 deletions lib/graphql/schema/field/unwrapped_resolve.rb

This file was deleted.

Loading