File: event.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (145 lines) | stat: -rw-r--r-- 6,115 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# frozen_string_literal: true
module GraphQL
  class Subscriptions
    # This thing can be:
    # - Subscribed to by `subscription { ... }`
    # - Triggered by `MySchema.subscriber.trigger(name, arguments, obj)`
    #
    class Event
      # @return [String] Corresponds to the Subscription root field name
      attr_reader :name

      # @return [GraphQL::Execution::Interpreter::Arguments]
      attr_reader :arguments

      # @return [GraphQL::Query::Context]
      attr_reader :context

      # @return [String] An opaque string which identifies this event, derived from `name` and `arguments`
      attr_reader :topic

      def initialize(name:, arguments:, field: nil, context: nil, scope: nil)
        @name = name
        @arguments = arguments
        @context = context
        field ||= context.field
        scope_key = field.subscription_scope
        scope_val = scope || (context && scope_key && context[scope_key])
        if scope_key &&
            (subscription = field.resolver) &&
            (subscription.respond_to?(:subscription_scope_optional?)) &&
            !subscription.subscription_scope_optional? &&
            scope_val.nil?
          raise Subscriptions::SubscriptionScopeMissingError, "#{field.path} (#{subscription}) requires a `scope:` value to trigger updates (Set `subscription_scope ..., optional: true` to disable this requirement)"
        end

        @topic = self.class.serialize(name, arguments, field, scope: scope_val, context: context)
      end

      # @return [String] an identifier for this unit of subscription
      def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance)
        subscription = field.resolver || GraphQL::Schema::Subscription
        normalized_args = stringify_args(field, arguments.to_h, context)
        subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
      end

      # @return [String] a logical identifier for this event. (Stable when the query is broadcastable.)
      def fingerprint
        @fingerprint ||= begin
          # When this query has been flagged as broadcastable,
          # use a generalized, stable fingerprint so that
          # duplicate subscriptions can be evaluated and distributed in bulk.
          # (`@topic` includes field, args, and subscription scope already.)
          if @context.namespace(:subscriptions)[:subscription_broadcastable]
            "#{@topic}/#{@context.query.fingerprint}"
          else
            # not broadcastable, build a unique ID for this event
            @context.schema.subscriptions.build_id
          end
        end
      end

      class << self
        private

        # This method does not support cyclic references in the Hash,
        # nor does it support Hashes whose keys are not sortable
        # with respect to their peers ( cases where a <=> b might throw an error )
        def deep_sort_hash_keys(hash_to_sort)
          raise ArgumentError.new("Argument must be a Hash") unless hash_to_sort.is_a?(Hash)
          hash_to_sort.keys.sort.map do |k|
            if hash_to_sort[k].is_a?(Hash)
              [k, deep_sort_hash_keys(hash_to_sort[k])]
            elsif hash_to_sort[k].is_a?(Array)
              [k, deep_sort_array_hashes(hash_to_sort[k])]
            else
              [k, hash_to_sort[k]]
            end
          end.to_h
        end

        def deep_sort_array_hashes(array_to_inspect)
          raise ArgumentError.new("Argument must be an Array") unless array_to_inspect.is_a?(Array)
          array_to_inspect.map do |v|
            if v.is_a?(Hash)
              deep_sort_hash_keys(v)
            elsif v.is_a?(Array)
              deep_sort_array_hashes(v)
            else
              v
            end
          end
        end

        def stringify_args(arg_owner, args, context)
          arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers
          case args
          when Hash
            next_args = {}
            args.each do |k, v|
              arg_name = k.to_s
              camelized_arg_name = GraphQL::Schema::Member::BuildType.camelize(arg_name)
              arg_defn = get_arg_definition(arg_owner, camelized_arg_name, context)
              arg_defn ||= get_arg_definition(arg_owner, arg_name, context)
              normalized_arg_name = arg_defn.graphql_name
              arg_base_type = arg_defn.type.unwrap
              # In the case where the value being emitted is seen as a "JSON"
              # type, treat the value as one atomic unit of serialization
              is_json_definition = arg_base_type && arg_base_type <= GraphQL::Types::JSON
              if is_json_definition
                sorted_value = if v.is_a?(Hash)
                  deep_sort_hash_keys(v)
                elsif v.is_a?(Array)
                  deep_sort_array_hashes(v)
                else
                  v
                end
                next_args[normalized_arg_name] = sorted_value.respond_to?(:to_json) ? sorted_value.to_json : sorted_value
              else
                next_args[normalized_arg_name] = stringify_args(arg_base_type, v, context)
              end
            end
            # Make sure they're deeply sorted
            next_args.sort.to_h
          when Array
            args.map { |a| stringify_args(arg_owner, a, context) }
          when GraphQL::Schema::InputObject
            stringify_args(arg_owner, args.to_h, context)
          else
            if arg_owner.is_a?(Class) && arg_owner < GraphQL::Schema::Enum
              # `prepare:` may have made the value something other than
              # a defined value of this enum -- use _that_ in this case.
              arg_owner.coerce_isolated_input(args) || args
            else
              args
            end
          end
        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 }
        end
      end
    end
  end
end