summaryrefslogtreecommitdiff
path: root/app/graphql/types/base_field.rb
blob: 5c8aabfe163f43a641f96270794c70c0caa543d5 (plain)
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
# frozen_string_literal: true

module Types
  class BaseField < GraphQL::Schema::Field
    prepend Gitlab::Graphql::Authorize
    include GitlabStyleDeprecations

    argument_class ::Types::BaseArgument

    DEFAULT_COMPLEXITY = 1

    def initialize(*args, **kwargs, &block)
      @calls_gitaly = !!kwargs.delete(:calls_gitaly)
      @constant_complexity = !!kwargs[:complexity]
      kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity])
      @feature_flag = kwargs[:feature_flag]
      kwargs = check_feature_flag(kwargs)
      kwargs = gitlab_deprecation(kwargs)

      super(*args, **kwargs, &block)
    end

    # Based on https://github.com/rmosolgo/graphql-ruby/blob/v1.11.4/lib/graphql/schema/field.rb#L538-L563
    # Modified to fix https://github.com/rmosolgo/graphql-ruby/issues/3113
    def resolve_field(obj, args, ctx)
      ctx.schema.after_lazy(obj) do |after_obj|
        query_ctx = ctx.query.context
        inner_obj = after_obj && after_obj.object

        ctx.schema.after_lazy(to_ruby_args(after_obj, args, ctx)) do |ruby_args|
          if authorized?(inner_obj, ruby_args, query_ctx)
            if @resolve_proc
              # We pass `after_obj` here instead of `inner_obj` because extensions expect a GraphQL::Schema::Object
              with_extensions(after_obj, ruby_args, query_ctx) do |extended_obj, extended_args|
                # Since `extended_obj` is now a GraphQL::Schema::Object, we need to get the inner object and pass that to `@resolve_proc`
                extended_obj = extended_obj.object if extended_obj.is_a?(GraphQL::Schema::Object)

                @resolve_proc.call(extended_obj, args, ctx)
              end
            else
              public_send_field(after_obj, ruby_args, query_ctx)
            end
          else
            err = GraphQL::UnauthorizedFieldError.new(object: inner_obj, type: obj.class, context: ctx, field: self)
            query_ctx.schema.unauthorized_field(err)
          end
        end
      end
    end

    def base_complexity
      complexity = DEFAULT_COMPLEXITY
      complexity += 1 if calls_gitaly?
      complexity
    end

    def calls_gitaly?
      @calls_gitaly
    end

    def constant_complexity?
      @constant_complexity
    end

    def visible?(context)
      return false if feature_flag.present? && !Feature.enabled?(feature_flag)

      super
    end

    private

    attr_reader :feature_flag

    def feature_documentation_message(key, description)
      "#{description}. Available only when feature flag `#{key}` is enabled"
    end

    def check_feature_flag(args)
      args[:description] = feature_documentation_message(args[:feature_flag], args[:description]) if args[:feature_flag].present?
      args.delete(:feature_flag)

      args
    end

    def field_complexity(resolver_class, current)
      return current if current.present? && current > 0

      if resolver_class
        field_resolver_complexity
      else
        base_complexity
      end
    end

    def field_resolver_complexity
      # Complexity can be either integer or proc. If proc is used then it's
      # called when computing a query complexity and context and query
      # arguments are available for computing complexity.  For resolvers we use
      # proc because we set complexity depending on arguments and number of
      # items which can be loaded.
      proc do |ctx, args, child_complexity|
        # Resolvers may add extra complexity depending on used arguments
        complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
        complexity += 1 if calls_gitaly?
        complexity += complexity * connection_complexity_multiplier(ctx, args)

        complexity.to_i
      end
    end

    def connection_complexity_multiplier(ctx, args)
      # Resolvers may add extra complexity depending on number of items being loaded.
      field_defn = to_graphql
      return 0 unless field_defn.connection?

      page_size   = field_defn.connection_max_page_size || ctx.schema.default_max_page_size
      limit_value = [args[:first], args[:last], page_size].compact.min
      multiplier  = self.resolver&.try(:complexity_multiplier, args).to_f
      limit_value * multiplier
    end
  end
end