Class: Rails::GraphQL::Request

Inherits:
Object
  • Object
show all
Extended by:
ActiveSupport::Autoload
Defined in:
lib/rails/graphql/request.rb,
lib/rails/graphql/request/event.rb,
lib/rails/graphql/request/errors.rb,
lib/rails/graphql/request/context.rb,
lib/rails/graphql/request/strategy.rb,
lib/rails/graphql/request/arguments.rb,
lib/rails/graphql/request/backtrace.rb,
lib/rails/graphql/request/component.rb,
lib/rails/graphql/request/subscription.rb,
lib/rails/graphql/request/prepared_data.rb,
lib/rails/graphql/request/component/field.rb,
lib/rails/graphql/request/component/spread.rb,
lib/rails/graphql/request/steps/preparable.rb,
lib/rails/graphql/request/steps/resolvable.rb,
lib/rails/graphql/request/steps/organizable.rb,
lib/rails/graphql/request/component/fragment.rb,
lib/rails/graphql/request/component/typename.rb,
lib/rails/graphql/request/helpers/directives.rb,
lib/rails/graphql/request/steps/authorizable.rb,
lib/rails/graphql/request/component/operation.rb,
lib/rails/graphql/request/helpers/selection_set.rb,
lib/rails/graphql/request/helpers/value_writers.rb,
lib/rails/graphql/request/strategy/cached_strategy.rb,
lib/rails/graphql/request/strategy/dynamic_instance.rb,
lib/rails/graphql/request/strategy/sequenced_strategy.rb,
lib/rails/graphql/request/strategy/multi_query_strategy.rb,
lib/rails/graphql/request/component/operation/subscription.rb

Overview

GraphQL Request

This class is responsible for processing a GraphQL response. It will handle queries, mutations, and subscription, as long as all of them are provided together. It also can be executed multiple times using the same context calling execute multiple times.

Options

  • :args - The arguments of the request, same as variables

  • :as - The format of the output of the request, supports both :hash and :string (defaults to :string)

  • :context - The context of the request, which can be accessed in fields, resolvers and so as a way to customize the result

  • :controller - From which controller this operation is running from, which provides view-like access to helpers and methods through the request

  • :namespace - Set what is the namespace used for the request (defaults to :base)

  • :operation_name - The name of the operation as a sort of label, it can also be collected by the name of the single operation in the request

  • :schema - The schema on which the request should run on. It has higher precedence than the namespace

  • :variables - The variables of the request

Defined Under Namespace

Modules: Authorizable, Backtrace, Directives, Organizable, Preparable, Resolvable, SelectionSet, ValueWriters Classes: Arguments, Component, Context, Errors, Event, PreparedData, Strategy, Subscription

Constant Summary collapse

RESPONSE_FORMATS =
{
  string: :to_json,
  object: :as_json,
  json: :as_json,
  hash: :as_json,
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema = nil, namespace: :base) ⇒ Request

Forces the schema to be registered on type map before moving forward



108
109
110
111
112
113
# File 'lib/rails/graphql/request.rb', line 108

def initialize(schema = nil, namespace: :base)
  @namespace = schema&.namespace || namespace
  @schema = GraphQL::Schema.find!(@namespace)

  ensure_schema!
end

Instance Attribute Details

#argsObject (readonly) Also known as: arguments

Returns the value of attribute args.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def args
  @args
end

#documentObject (readonly)

Returns the value of attribute document.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def document
  @document
end

#errorsObject (readonly)

Returns the value of attribute errors.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def errors
  @errors
end

#fragmentsObject (readonly)

Returns the value of attribute fragments.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def fragments
  @fragments
end

#operation_nameObject (readonly)

Returns the value of attribute operation_name.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def operation_name
  @operation_name
end

#operationsObject (readonly)

Returns the value of attribute operations.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def operations
  @operations
end

#originObject (readonly) Also known as: controller, channel

Returns the value of attribute origin.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def origin
  @origin
end

#responseObject (readonly)

Returns the value of attribute response.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def response
  @response
end

#schemaObject (readonly)

Returns the value of attribute schema.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def schema
  @schema
end

#stackObject (readonly)

Returns the value of attribute stack.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def stack
  @stack
end

#strategyObject (readonly)

Returns the value of attribute strategy.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def strategy
  @strategy
end

#subscriptionsObject (readonly)

Returns the value of attribute subscriptions.



65
66
67
# File 'lib/rails/graphql/request.rb', line 65

def subscriptions
  @subscriptions
end

Class Method Details

.compile(*args, schema: nil, namespace: :base, **xargs) ⇒ Object

Shortcut for initialize and compile



87
88
89
# File 'lib/rails/graphql/request.rb', line 87

def compile(*args, schema: nil, namespace: :base, **xargs)
  new(schema, namespace: namespace).compile(*args, **xargs)
end

.const_defined?(name) ⇒ Boolean

Allow accessing component-based objects through the request

Returns:

  • (Boolean)


97
98
99
# File 'lib/rails/graphql/request.rb', line 97

def const_defined?(name, *)
  Component.const_defined?(name, false) || super
end

.const_missing(name) ⇒ Object

Allow accessing component-based objects through the request



102
103
104
# File 'lib/rails/graphql/request.rb', line 102

def const_missing(name)
  Component.const_defined?(name, false) ? Component.const_get(name, false) : super
end

.execute(*args, schema: nil, namespace: :base, context: {}, **xargs) ⇒ Object

Shortcut for initialize, set context, and execute



80
81
82
83
84
# File 'lib/rails/graphql/request.rb', line 80

def execute(*args, schema: nil, namespace: :base, context: {}, **xargs)
  result = new(schema, namespace: namespace)
  result.context = context if context.present?
  result.execute(*args, **xargs)
end

.valid?(*args, schema: nil, namespace: :base, **xargs) ⇒ Boolean

Shortcut for initialize and validate

Returns:

  • (Boolean)


92
93
94
# File 'lib/rails/graphql/request.rb', line 92

def valid?(*args, schema: nil, namespace: :base, **xargs)
  new(schema, namespace: namespace).valid?(*args, **xargs)
end

Instance Method Details

#build(klass, *args, &block) ⇒ Object

This initiates a new object which is aware of class extensions



301
302
303
304
305
306
# File 'lib/rails/graphql/request.rb', line 301

def build(klass, *args, &block)
  ext_module = class_extensions[klass]
  obj = klass.new(*args, &block)
  obj.extend(ext_module) if ext_module
  obj
end

#build_from_cache(klass) ⇒ Object

This allocates a new object which is aware of class extensions



309
310
311
312
313
314
# File 'lib/rails/graphql/request.rb', line 309

def build_from_cache(klass)
  ext_module = class_extensions[klass]
  obj = klass.allocate
  obj.extend(ext_module) if ext_module
  obj
end

#build_rescue_object(**extra) ⇒ Object

Build a easy-to-access object representing the current information of the execution to be used on rescue_with_handler



229
230
231
232
233
234
235
236
237
# File 'lib/rails/graphql/request.rb', line 229

def build_rescue_object(**extra)
  OpenStruct.new(extra.reverse_merge(
    args: @args,
    source: stack.first,
    request: self,
    response: @response,
    document: @document,
  )).freeze
end

#cache(key, init_value = nil, &block) ⇒ Object

A shared way to cache information across the execution of an request



317
318
319
# File 'lib/rails/graphql/request.rb', line 317

def cache(key, init_value = nil, &block)
  @cache[key] ||= (init_value || block&.call || {})
end

#cache_dumpObject

Build the object that represent the request in the cache format



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/rails/graphql/request.rb', line 350

def cache_dump
  {
    strategy: @strategy.cache_dump,
    operation_name: @operation_name,
    type_map_version: schema.version,
    document: @document,
    errors: @errors.cache_dump,
    operations: @operations.transform_values(&:cache_dump),
    fragments: @fragments&.transform_values { |f| f.try(:cache_dump) }&.compact,
  }
end

#cache_load(data) ⇒ Object

Read the request from the cache to run it faster



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/rails/graphql/request.rb', line 363

def cache_load(data)
  version = data[:type_map_version]
  @document = data[:document]
  @operation_name = data[:operation_name]
  resolve_from_cache = (version == schema.version)

  # Run the document from scratch if TypeMap has changed
  # TODO: We need to save the new organized document
  return run_document unless resolve_from_cache
  @valid_cache = true unless defined?(@valid_cache)

  # Run the document as a cached operation
  errors.cache_load(data[:errors])
  @strategy = build(data[:strategy][:class], self)
  @strategy.trigger_event(:request)
  @strategy.cache_load(data)
  @strategy.resolve!
end

#compile(document, compress: true) ⇒ Object

Compile a given document



158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/rails/graphql/request.rb', line 158

def compile(document, compress: true)
  reset!

  log_execution(document, event: 'compile.graphql') do
    @document = initialize_document(document)
    run_document(with: :compile)

    result = Marshal.dump(cache_dump)
    result = Zlib.deflate(result) if compress

    @log_extra[:total] = result.bytesize
    result
  end
end

#contextObject

Get the context of the request



121
122
123
# File 'lib/rails/graphql/request.rb', line 121

def context
  @context ||= OpenStruct.new.freeze
end

#context=(data) ⇒ Object

Set the context of the request, it must be a Hash



126
127
128
# File 'lib/rails/graphql/request.rb', line 126

def context=(data)
  @context = build_ostruct(data).freeze
end

#exception_to_error(exception, node, **xargs) ⇒ Object

Add the given exception to the errors using the node location



246
247
248
249
# File 'lib/rails/graphql/request.rb', line 246

def exception_to_error(exception, node, **xargs)
  xargs[:exception] = exception.class.name
  report_node_error(xargs.delete(:message) || exception.message, node, **xargs)
end

#execute(document, **xargs) ⇒ Object Also known as: perform

Execute a given document with the given arguments



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/rails/graphql/request.rb', line 136

def execute(document, **xargs)
  output = xargs.delete(:as) || schema.config.default_response_format
  cache = xargs.delete(:hash)
  formatter = RESPONSE_FORMATS[output]

  document, cache = nil, document if xargs.delete(:compiled)
  prepared_data = xargs.delete(:data_for)
  reset!(**xargs)

  @response = initialize_response(output, formatter)
  import_prepared_data(prepared_data)
  execute!(document, cache)

  response.public_send(formatter)
rescue StaticResponse
  # TODO: Maybe change this to a throw/catch instead
  response.public_send(formatter)
end

#extend(*modules) ⇒ Object

Add class extensions to the request, which ensures a bunch of extended behaviors for all the objects created through the request



294
295
296
297
298
# File 'lib/rails/graphql/request.rb', line 294

def extend(*modules)
  import_class_extensions(*modules)
  request_ext = class_extensions[self.class]
  super(request_ext) if request_ext && !is_a?(request_ext)
end

#extensionsObject

Allow adding extra information to the response, in a extensions key



131
132
133
# File 'lib/rails/graphql/request.rb', line 131

def extensions
  @extensions ||= {}
end

#force_response(response, error = StaticResponse) ⇒ Object

This is used by cache and static responses to jump from executing to delivery a response right away



187
188
189
190
191
# File 'lib/rails/graphql/request.rb', line 187

def force_response(response, error = StaticResponse)
  return unless defined?(@response)
  @response = response
  raise error
end

#import_prepared_data(prepared_data) ⇒ Object

Import prepared data that is formatted as a hash



194
195
196
197
198
# File 'lib/rails/graphql/request.rb', line 194

def import_prepared_data(prepared_data)
  prepared_data&.each do |key, value|
    prepare_data_for(key, value)
  end
end

#location_of(node) ⇒ Object

Get the location object of a given node



258
259
260
261
262
263
264
265
266
# File 'lib/rails/graphql/request.rb', line 258

def location_of(node)
  node = node.instance_variable_get(:@node) if node.is_a?(Request::Component)
  return unless node.is_a?(GQLParser::Token)

  [
    { 'line' => node.begin_line, 'column' => node.begin_column },
    { 'line' => node.end_line,   'column' => node.end_column },
  ]
end

#nested_cache(key, sub_key) ⇒ Object

A better way to ensure that nil values in a hash cache won’t be reinitialized



323
324
325
# File 'lib/rails/graphql/request.rb', line 323

def nested_cache(key, sub_key)
  (source = cache(key)).key?(sub_key) ? source[sub_key] : source[sub_key] = yield
end

#prepare_data_for(field, value, **options) ⇒ Object

Add a new prepared data from value to the given field



201
202
203
204
205
206
207
208
209
# File 'lib/rails/graphql/request.rb', line 201

def prepare_data_for(field, value, **options)
  field = PreparedData.lookup(self, field)

  if prepared_data.key?(field)
    prepared_data[field].push(value)
  else
    prepared_data[field] = PreparedData.new(field, value, **options)
  end
end

#prepared_data_for(field) ⇒ Object

Recover the next prepared data for the given field



212
213
214
215
216
217
# File 'lib/rails/graphql/request.rb', line 212

def prepared_data_for(field)
  return unless defined?(@prepared_data)

  field = field.field if field.is_a?(Component::Field)
  prepared_data[field]
end

#prepared_data_for?(field) ⇒ Boolean

Check if the given field has prepared data

Returns:

  • (Boolean)


220
221
222
223
224
225
# File 'lib/rails/graphql/request.rb', line 220

def prepared_data_for?(field)
  return false unless defined?(@prepared_data)

  field = field.field if field.is_a?(Component::Field)
  prepared_data.key?(field)
end

#read_cache_request(data = @document) ⇒ Object

Read the request from the cache to run it faster



338
339
340
341
342
343
344
345
346
347
# File 'lib/rails/graphql/request.rb', line 338

def read_cache_request(data = @document)
  begin
    data = Zlib.inflate(data) if data[0] == 'x'
    data = Marshal.load(data)
  rescue Zlib::BufError, ArgumentError
    raise ::ArgumentError, +'Unable to recover the cached request.'
  end

  cache_load(data)
end

#report_error(message, **xargs) ⇒ Object

The final helper that facilitates how errors are reported



269
270
271
272
273
274
# File 'lib/rails/graphql/request.rb', line 269

def report_error(message, **xargs)
  xargs[:path] ||= stack_to_path
  errors.add(message, **xargs)

  nil # Return nil for easier usage
end

#report_node_error(message, node, **xargs) ⇒ Object

A little helper to report an error on a given node



252
253
254
255
# File 'lib/rails/graphql/request.rb', line 252

def report_node_error(message, node, **xargs)
  xargs[:locations] ||= location_of(node)
  report_error(message, **xargs)
end

#rescue_with_handler(exception, **extra) ⇒ Object

Use schema handlers for exceptions caught during the execution process



240
241
242
243
# File 'lib/rails/graphql/request.rb', line 240

def rescue_with_handler(exception, **extra)
  ExtendedError.extend(exception, build_rescue_object(**extra))
  schema.rescue_with_handler(exception)
end

#stack_to_pathObject

Convert the current stack into a error path ignoring the schema



286
287
288
289
290
# File 'lib/rails/graphql/request.rb', line 286

def stack_to_path
  stack[0..-2].map do |item|
    item.is_a?(Numeric) ? item : item.try(:gql_name)
  end.compact.reverse
end

#stacked(object, &block) ⇒ Object

Add the given object into the execution stack and execute the given block making sure to rescue exceptions using the rescue_with_handler



278
279
280
281
282
283
# File 'lib/rails/graphql/request.rb', line 278

def stacked(object, &block)
  stack.unshift(object)
  block.call
ensure
  stack.shift
end

#subscriptions?Boolean

Check if any new subscription was added

Returns:

  • (Boolean)


116
117
118
# File 'lib/rails/graphql/request.rb', line 116

def subscriptions?
  defined?(@subscriptions) && @subscriptions.any?
end

#valid?(document) ⇒ Boolean

Check if the given document is valid by piggybacking on the compile process

Returns:

  • (Boolean)


175
176
177
178
179
180
181
182
183
# File 'lib/rails/graphql/request.rb', line 175

def valid?(document)
  reset!

  log_execution(document, event: 'validate.graphql') do
    @document = initialize_document(document)
    run_document(with: :compile)
    @log_extra[:result] = @errors.empty?
  end
end

#valid_cache?Boolean

Show if the current cached operation is still valid

Returns:

  • (Boolean)


328
329
330
# File 'lib/rails/graphql/request.rb', line 328

def valid_cache?
  defined?(@valid_cache) && @valid_cache
end

#write_cache_request(hash, data = cache_dump) ⇒ Object

Write the request into the cache so it can run again faster



333
334
335
# File 'lib/rails/graphql/request.rb', line 333

def write_cache_request(hash, data = cache_dump)
  schema.write_on_cache(hash, Marshal.dump(data))
end