lib/rails/graphql/request/component/field.rb in rails-graphql-0.2.1 vs lib/rails/graphql/request/component/field.rb in rails-graphql-1.0.0.beta

- old
+ new

@@ -1,10 +1,10 @@ # frozen_string_literal: true -module Rails # :nodoc: - module GraphQL # :nodoc: - class Request # :nodoc: +module Rails + module GraphQL + class Request # = GraphQL Request Component Field # # This class holds information about a given field that should be # collected from the source of where it was requested. class Component::Field < Component @@ -12,65 +12,77 @@ include ValueWriters include SelectionSet include Directives delegate :decorate, to: :type_klass - delegate :operation, :variables, to: :parent - delegate :method_name, :resolver, :performer, :type_klass, :leaf_type?, - :dynamic_resolver?, to: :field + delegate :operation, :variables, :request, to: :parent + delegate :method_name, :resolver, :performer, :type_klass,:leaf_type?, + :dynamic_resolver?, :mutation?, to: :field - parent_memoize :request - attr_reader :name, :alias_name, :parent, :field, :arguments, :current_object alias args arguments - def initialize(parent, node, data) + def initialize(parent, node) @parent = parent - @name = data[:name] - @alias_name = data[:alias] + @name = node[0] + @alias_name = node[1] - super(node, data) + super(node) end - # Return both the field directives and the request directives - def all_directives - field.all_directives + super - end - # Override that considers the requested field directives and also the # definition field events, both from itself and its directives events def all_listeners - (request.cache(:listeners)[field] ||= field.all_listeners) + super + request.nested_cache(:listeners, field) do + if !field.listeners? + directive_listeners + elsif !directives? + field.all_listeners + else + local = directive_listeners + local.empty? ? field.all_listeners : field.all_listeners + local + end + end end # Override that considers the requested field directives and also the # definition field events, both from itself and its directives events def all_events - @all_events ||= Helpers.merge_hash_array( - (request.cache(:events)[field] ||= field.all_events), - super, - ) + request.nested_cache(:events, field) do + if !field.events? + directive_events + elsif !directives? + field.all_events + else + Helpers.merge_hash_array(field.all_events, directive_events) + end + end end # Get and cache all the arguments for the field def all_arguments - request.cache(:arguments)[field] ||= begin - if (result = field.all_arguments).any? - result.each_value.map(&:gql_name).zip(result.each_value).to_h - else - {} + return unless field.arguments? + + request.nested_cache(:arguments, field) do + field.all_arguments.each_value.with_object({}) do |argument, hash| + hash[argument.gql_name] = argument end end end + # Check if the field is using a directive + def using?(item_or_symbol) + super || field.using?(item_or_symbol) + end + # Assign a given +field+ to this class. The field must be an output # field, which means that +output_type?+ must be true. It also must be # called exactly once per field. - def assing_to(field) - raise ArgumentError, <<~MSG.squish if defined?(@assigned) + def assign_to(field) + raise ArgumentError, (+<<~MSG).squish if defined?(@assigned) The "#{gql_name}" field is already assigned to #{@field.inspect}. MSG @field = field end @@ -95,20 +107,28 @@ # need to be assigned to a filed def assignable? true end + # Check if all the sub fields are broadcastable + # TODO: Maybe check for interfaces and if all types allow broadcast + def broadcastable? + value = field.broadcastable? + value = schema.config.default_subscription_broadcastable if value.nil? + value != false + end + # A little extension of the +is_a?+ method that allows checking it using # the underlying +field+ def of_type?(klass) super || field.of_type?(klass) end # When the field is invalid, there's no much to do # TODO: Maybe add a invalid event trigger here def resolve_invalid(error = nil) - request.exception_to_error(error, @node) if error.present? + request.exception_to_error(error, self) if error.present? validate_output!(nil) response.safe_add(gql_name, nil) rescue InvalidValueError raise unless entry_point? @@ -116,30 +136,48 @@ # When the +type_klass+ of an object is an interface or a union, the # field needs to be redirected to the one from the actual resolved # +object+ type def resolve_with!(object) + return if skipped? return resolve! if invalid? old_field, @field = @field, object[@field.name] + request.nested_cache(:listeners, field) { strategy.add_listeners_from(self) } @current_object = object resolve! ensure @field, @current_object = old_field, nil end + # Build the cache object + def cache_dump + super.merge(field: (field && all_to_gid(field))) + end + + # Organize from cache data + def cache_load(data) + @name = data[:node][0] + @alias_name = data[:node][1] + @field = all_from_gid(data[:field]) + super + + check_authorization! unless unresolvable? + end + protected # Perform the organization step def organize_then(&block) super(block) do check_assignment! + + parse_directives(@node[3]) check_authorization! - parse_arguments - parse_directives - parse_selection + parse_arguments(@node[2]) + parse_selection(@node[4]) end end # Perform the prepare step def prepare_then(&block) @@ -147,12 +185,11 @@ end # Perform the resolve step def resolve_then(&block) stacked do - strategy.perform(self) if field.mutation? - send(field.array? ? 'resolve_many' : 'resolve_one', &block) + send((field.array? ? :resolve_many : :resolve_one), &block) rescue StandardError => error resolve_invalid(error) end end @@ -179,49 +216,33 @@ trigger_event(:finalize) end end end - # This override allows reasigned fields to perform events. This - # happens when fields are originally organized from interfaces. If - # the event is stopped for the object, then it doesn't proceed to the - # strategy implementation, ensuring compatibility - def trigger_event(event_name, **xargs) - return super if !defined?(@current_object) || @current_object.nil? - - listeners = request.cache(:listeners)[field] ||= field.all_listeners - return super unless listeners.include?(event_name) - - callbacks = request.cache(:events)[field] ||= field.all_events - old_events, @all_events = @all_events, callbacks - super - ensure - @all_events = old_events - end - # Check if the field was assigned correctly to an output field def check_assignment! - raise MissingFieldError, <<~MSG.squish if field.nil? + raise MissingFieldError, (+<<~MSG).squish if field.nil? Unable to find a field named "#{gql_name}" on #{entry_point? ? operation.kind : parent.type_klass.name}. MSG - raise FieldError, <<~MSG.squish unless field.output_type? + raise FieldError, (+<<~MSG).squish unless field.output_type? The "#{gql_name}" was assigned to a non-output type of field: #{field.inspect}. MSG - empty_selection = data[:selection].nil? || data[:selection].null? - raise FieldError, <<~MSG.squish if field.leaf_type? && !empty_selection - The "#{gql_name}" was assigned to the #{type_klass.gql_name} which - is a leaf type and does not have nested fields. - MSG + if @node[4].nil? + raise FieldError, (+<<~MSG).squish if !field.leaf_type? + The "#{gql_name}" was assigned to the #{type_klass.gql_name} which + is not a leaf type and requires a selection of fields. + MSG + else + raise FieldError, (+<<~MSG).squish if field.leaf_type? + The "#{gql_name}" was assigned to the #{type_klass.gql_name} which + is a leaf type and does not have nested fields. + MSG + end - raise FieldError, <<~MSG.squish if !field.leaf_type? && empty_selection - The "#{gql_name}" was assigned to the #{type_klass.gql_name} which - is not a leaf type and requires a selection of fields. - MSG - - raise DisabledFieldError, <<~MSG.squish if field.disabled? + raise DisabledFieldError, (+<<~MSG).squish if field.disabled? The "#{gql_name}" was found but it is marked as disabled. MSG end end end