summaryrefslogtreecommitdiff
path: root/app/graphql/gitlab_schema.rb
blob: 37adf4c2d3b19a22c47d5471811f1857005a9567 (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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# frozen_string_literal: true

class GitlabSchema < GraphQL::Schema
  # Currently an IntrospectionQuery has a complexity of 179.
  # These values will evolve over time.
  DEFAULT_MAX_COMPLEXITY = 200
  AUTHENTICATED_MAX_COMPLEXITY = 250
  ADMIN_MAX_COMPLEXITY = 300

  DEFAULT_MAX_DEPTH = 15
  AUTHENTICATED_MAX_DEPTH = 20

  # Tracers (order is important)
  use Gitlab::Graphql::Tracers::ApplicationContextTracer
  use Gitlab::Graphql::Tracers::MetricsTracer
  use Gitlab::Graphql::Tracers::LoggerTracer

  # TODO: Old tracer which will be removed eventually
  #       See https://gitlab.com/gitlab-org/gitlab/-/issues/345396
  use Gitlab::Graphql::GenericTracing
  use Gitlab::Graphql::Tracers::TimerTracer

  use GraphQL::Subscriptions::ActionCableSubscriptions
  use BatchLoader::GraphQL
  use Gitlab::Graphql::Pagination::Connections
  use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout

  query_analyzer Gitlab::Graphql::QueryAnalyzers::AST::LoggerAnalyzer
  query_analyzer Gitlab::Graphql::QueryAnalyzers::AST::RecursionAnalyzer

  query Types::QueryType
  mutation Types::MutationType
  subscription Types::SubscriptionType

  default_max_page_size 100

  validate_max_errors 5
  validate_timeout 0.2.seconds

  lazy_resolve ::Gitlab::Graphql::Lazy, :force

  class << self
    def multiplex(queries, **kwargs)
      kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless kwargs.key?(:max_complexity)

      queries.each do |query|
        query[:max_complexity] ||= max_query_complexity(query[:context]) unless query.key?(:max_complexity)
        query[:max_depth] = max_query_depth(query[:context]) unless query.key?(:max_depth)
      end

      super(queries, **kwargs)
    end

    def get_type(type_name, context = GraphQL::Query::NullContext)
      type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name)
      type_name = Gitlab::Graphql::TypeNameDeprecations.apply_to_graphql_name(type_name)

      super(type_name, context)
    end

    def id_from_object(object, _type = nil, _ctx = nil)
      unless object.respond_to?(:to_global_id)
        # This is an error in our schema and needs to be solved. So raise a
        # more meaningful error message
        raise "#{object} does not implement `to_global_id`. "\
              "Include `GlobalID::Identification` into `#{object.class}"
      end

      object.to_global_id
    end

    # Find an object by looking it up from its global ID, passed as a string.
    #
    # This is the composition of 'parse_gid' and 'find_by_gid', see these
    # methods for further documentation.
    def object_from_id(global_id, ctx = {})
      gid = parse_gid(global_id, ctx)

      find_by_gid(gid)
    end

    def resolve_type(type, object, ctx = :__undefined__)
      return if type.respond_to?(:assignable?) && !type.assignable?(object)

      super
    end

    # Find an object by looking it up from its 'GlobalID'.
    #
    # * For `ApplicationRecord`s, this is equivalent to
    #   `global_id.model_class.find(gid.model_id)`, but more efficient.
    # * For classes that implement `.lazy_find(global_id)`, this class method
    #   will be called.
    # * All other classes will use `GlobalID#find`
    def find_by_gid(gid)
      return unless gid

      if gid.model_class < ApplicationRecord
        Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
      elsif gid.model_class.respond_to?(:lazy_find)
        gid.model_class.lazy_find(gid.model_id)
      else
        gid.find
      end
    end

    # Parse a string to a GlobalID, raising ArgumentError if there are problems
    # with it.
    #
    # Problems that may occur:
    #  * it may not be syntactically valid
    #  * it may not match the expected type (see below)
    #
    # Options:
    #  * :expected_type [Class] - the type of object this GlobalID should refer to.
    #  * :expected_type [[Class]] - array of the types of object this GlobalID should refer to.
    #
    # e.g.
    #
    # ```
    #   gid = GitlabSchema.parse_gid(my_string, expected_type: ::Project)
    #   project_id = gid.model_id
    #   gid.model_class == ::Project
    # ```
    def parse_gid(global_id, ctx = {})
      expected_types = Array(ctx[:expected_type])
      gid = GlobalID.parse(global_id)

      raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid

      if expected_types.any? && expected_types.none? { |type| gid.model_class.ancestors.include?(type) }
        vars = { global_id: global_id, expected_types: expected_types.join(', ') }
        msg = _('%{global_id} is not a valid ID for %{expected_types}.') % vars
        raise Gitlab::Graphql::Errors::ArgumentError, msg
      end

      gid
    end

    # Parse an array of strings to an array of GlobalIDs, raising ArgumentError if there are problems
    # with it.
    # See #parse_gid
    #
    # ```
    #   gids = GitlabSchema.parse_gids(my_array_of_strings, expected_type: ::Project)
    #   project_ids = gids.map(&:model_id)
    #   gids.all? { |gid| gid.model_class == ::Project }
    # ```
    def parse_gids(global_ids, ctx = {})
      global_ids.map { |gid| parse_gid(gid, ctx) }
    end

    private

    def max_query_complexity(ctx)
      current_user = ctx&.fetch(:current_user, nil)

      if current_user&.admin
        ADMIN_MAX_COMPLEXITY
      elsif current_user
        AUTHENTICATED_MAX_COMPLEXITY
      else
        DEFAULT_MAX_COMPLEXITY
      end
    end

    def max_query_depth(ctx)
      current_user = ctx&.fetch(:current_user, nil)

      if current_user
        AUTHENTICATED_MAX_DEPTH
      else
        DEFAULT_MAX_DEPTH
      end
    end
  end

  def get_type(type_name)
    type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name)
    type_name = Gitlab::Graphql::TypeNameDeprecations.apply_to_graphql_name(type_name)

    super(type_name)
  end
end

GitlabSchema.prepend_mod_with('GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule