Class: Gitlab::Triage::NetworkAdapters::GraphqlAdapter

Inherits:
BaseAdapter
  • Object
show all
Defined in:
lib/gitlab/triage/network_adapters/graphql_adapter.rb

Defined Under Namespace

Classes: RawDocument

Constant Summary collapse

Client =
GraphQL::Client

Constants inherited from BaseAdapter

BaseAdapter::USER_AGENT

Instance Attribute Summary

Attributes inherited from BaseAdapter

#options

Instance Method Summary collapse

Methods inherited from BaseAdapter

#initialize

Constructor Details

This class inherits a constructor from Gitlab::Triage::NetworkAdapters::BaseAdapter

Instance Method Details

#build_graphql_response(node, headers) ⇒ Object (private)

Shared response assembly for both #query and #mutate (parsed path) and #query_raw (raw path). Takes a plain-Ruby node (Hash/Array/nil) and the rate-limit headers, and produces the canonical response shape: { ratelimit_*, [more_pages, end_cursor], results }.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 79

def build_graphql_response(node, headers)
  response = {
    ratelimit_remaining: rate_limit_remaining(headers),
    ratelimit_reset_at: rate_limit_reset_at(headers)
  }

  return response.merge(results: {}) if node.nil?

  if node.is_a?(Hash) && node.key?('nodes')
    page_info = node['pageInfo'] || {}
    response.merge(
      more_pages: page_info['hasNextPage'] || false,
      end_cursor: page_info['endCursor'],
      results: node['nodes']
    )
  else
    response.merge(results: node)
  end
end

#clientObject (private)



172
173
174
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 172

def client
  @client ||= Client.new(schema: schema, execute: http_client).tap { |client| client.allow_dynamic_queries = true }
end

#http_clientObject (private)



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
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 140

def http_client
  Client::HTTP.new("#{options.host_url}/api/graphql") do
    def execute(document:, operation_name: nil, variables: {}, context: {}) # rubocop:disable Lint/NestedMethodDefinition
      body = {}
      body['query'] = document.to_query_string
      body['variables'] = variables if variables.any?
      body['operationName'] = operation_name if operation_name

      response = HTTParty.post(
        uri,
        body: body.to_json,
        headers: {
          'User-Agent' => USER_AGENT,
          'Content-type' => 'application/json',
          'PRIVATE-TOKEN' => context[:token]
        }
      )

      case response.code
      when 200, 400
        JSON.parse(response.body).merge('extensions' => { 'headers' => response.headers })
      else
        { 'errors' => [{ 'message' => "#{response.code} #{response.message}" }] }
      end
    end
  end
end

#mutate(graphql_mutation, variables: {}) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 56

def mutate(graphql_mutation, variables: {})
  response = client.query(graphql_mutation, variables: variables, context: { token: options.token })

  raise_on_error!(response)

  parsed_response = response.data
  headers = response.extensions.fetch('headers', {})

  {
    ratelimit_remaining: headers['ratelimit-remaining'].to_i,
    ratelimit_reset_at: Time.at(headers['ratelimit-reset'].to_i),
    results: parsed_response.to_h
  }
end

#parse_response(response, resource_path) ⇒ Object (private)



128
129
130
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 128

def parse_response(response, resource_path)
  resource_path.reduce(response.data) { |data, resource| data&.send(resource) } # rubocop:disable GitlabSecurity/PublicSend
end

#plain_node(parsed_response) ⇒ Object (private)

Converts a parsed GraphQL::Client response node into plain Ruby so the shared builder only ever deals with Hash/Array, never client objects.



114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 114

def plain_node(parsed_response)
  return if parsed_response.nil?
  return parsed_response.map(&:to_h) if parsed_response.is_a?(Client::List)
  return parsed_response.to_h unless parsed_response.nodes?

  {
    'nodes' => parsed_response.nodes.map(&:to_h),
    'pageInfo' => {
      'hasNextPage' => parsed_response.page_info.has_next_page,
      'endCursor' => parsed_response.page_info.end_cursor
    }
  }
end

#query(graphql_query, resource_path: [], variables: {}) ⇒ Object



22
23
24
25
26
27
28
29
30
31
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 22

def query(graphql_query, resource_path: [], variables: {})
  response = client.query(graphql_query, variables: variables, context: { token: options.token })

  raise_on_error!(response)

  headers = response.extensions.fetch('headers', {})
  node = plain_node(parse_response(response, resource_path))

  build_graphql_response(node, headers)
end

#query_raw(query_string, resource_path: [], variables: {}) ⇒ Object

Executes a raw query string directly through the HTTP layer, skipping GraphQL::Client schema validation (client.parse). Required for queries that reference experiment-tagged fields/arguments: GitLab hides those from introspection but executes them at runtime, so the client-side validator would otherwise reject a perfectly valid query.

Returns the same shape as #query so callers can be source-agnostic.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 40

def query_raw(query_string, resource_path: [], variables: {})
  raw = http_client.execute(
    document: RawDocument.new(query_string),
    variables: variables,
    context: { token: options.token }
  )

  errors = raw['errors']
  raise "There was an error: #{errors.to_json}" if errors.present?

  headers = raw.dig('extensions', 'headers') || {}
  node = resource_path.reduce(raw['data']) { |data, segment| data&.fetch(segment, nil) }

  build_graphql_response(node, headers)
end

#raise_on_error!(response) ⇒ Object (private)



132
133
134
135
136
137
138
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 132

def raise_on_error!(response)
  return if response.errors.blank?

  puts Gitlab::Triage::UI.debug response.inspect if options.debug

  raise "There was an error: #{response.errors.messages.to_json}"
end

#rate_limit_remaining(headers) ⇒ Object (private)

When the rate-limit headers are absent (e.g. a server that doesn’t send them), treat the limit as not-a-concern rather than coercing a missing header to 0/1970, which would otherwise trip rate_limit_wait.



102
103
104
105
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 102

def rate_limit_remaining(headers)
  value = headers['ratelimit-remaining']
  value.present? ? value.to_i : Float::INFINITY
end

#rate_limit_reset_at(headers) ⇒ Object (private)



107
108
109
110
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 107

def rate_limit_reset_at(headers)
  value = headers['ratelimit-reset']
  Time.at(value.to_i) if value.present?
end

#schemaObject (private)



168
169
170
# File 'lib/gitlab/triage/network_adapters/graphql_adapter.rb', line 168

def schema
  @schema ||= Client.load_schema(http_client)
end