lib/graphql/subscriptions/broadcast_analyzer.rb



# frozen_string_literal: true

module GraphQL
  class Subscriptions
    # Detect whether the current operation:
    # - Is a subscription operation
    # - Is completely broadcastable
    #
    # Assign the result to `context.namespace(:subscriptions)[:subscription_broadcastable]`
    # @api private
    # @see Subscriptions#broadcastable? for a public API
    class BroadcastAnalyzer < GraphQL::Analysis::AST::Analyzer
      def initialize(subject)
        super
        @default_broadcastable = subject.schema.subscriptions.default_broadcastable
        # Maybe this will get set to false while analyzing
        @subscription_broadcastable = true
      end

      # Only analyze subscription operations
      def analyze?
        @query.subscription?
      end

      def on_enter_field(node, parent, visitor)
        if (@subscription_broadcastable == false) || visitor.skipping?
          return
        end

        current_field = visitor.field_definition
        apply_broadcastable(current_field)

        current_type = visitor.parent_type_definition
        if current_type.kind.interface?
          pt = @query.possible_types(current_type)
          pt.each do |object_type|
            ot_field = @query.get_field(object_type, current_field.graphql_name)
            # Inherited fields would be exactly the same object;
            # only check fields that are overrides of the inherited one
            if ot_field && ot_field != current_field
              apply_broadcastable(ot_field)
            end
          end
        end
      end

      # Assign the result to context.
      # (This method is allowed to return an error, but we don't need to)
      # @return [void]
      def result
        query.context.namespace(:subscriptions)[:subscription_broadcastable] = @subscription_broadcastable
        nil
      end

      private

      # Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value)
      def apply_broadcastable(field_defn)
        current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable?
        case current_field_broadcastable
        when nil
          # If the value wasn't set, mix in the default value:
          # - If the default is false and the current value is true, make it false
          # - If the default is true and the current value is true, it stays true
          # - If the default is false and the current value is false, keep it false
          # - If the default is true and the current value is false, keep it false
          @subscription_broadcastable = @subscription_broadcastable && @default_broadcastable
        when false
          # One non-broadcastable field is enough to make the whole subscription non-broadcastable
          @subscription_broadcastable = false
        when true
          # Leave `@broadcastable_query` true if it's already true,
          # but don't _set_ it to true if it was set to false by something else.
          # Actually, just leave it!
        else
          raise ArgumentError, "Unexpected `.broadcastable?` value for #{field_defn.path}: #{current_field_broadcastable}"
        end
      end
    end
  end
end