Table of Contents
Request
A request is where everything comes together to deliver responses for GraphQL documents.
The Request
class and its children classes perform a series of steps to fulfill
your document, which are:
- Initializing - Before a response is started
- Organizing - Collecting fields and event listeners
- Preparing - Preparing the data and performing mutations
- Resolving - Formatting the response
1. Initializing
This is the only step that is not protected against exceptions. Any errors that may occur during the initialization will be raised, and they are probably related to a wrong setup of the request, like not being able to find a schema.
We can divide this process into four parts:
1. 1. Instantiating
When a request is instantiated, it requires a schema, either the class or its namespace.
request = Rails::GraphQL.request(GraphQL::AppSchema)
# OR
request = Rails::GraphQL.request(namespace: :base)
# OR, same as above
request = Rails::GraphQL.request
# OR
request = GraphQL::AppSchema.request
1. 2. Setting Up
This step is where you can prepare the ground for your request to be executed smoothly. Here is when you can add a context or prepare data.
Context
Context is a way for you to provide external information to the internal processing
of the requests. The context is also important for subscriptions because it
provides the values to their scope. Once set, the context is turned into an
OpenStruct
,
facilitating reading.
request.context = { current_user: current_user }
# THEN
request.context.current_user
Important The context is immutable! If you need to store any data during the request, you should use the operation memo.
Prepared Data
Preparing data allows you to override what underlying data will be used to resolve the field. This is useful for testing and triggering subscriptions.
request.prepare_data_for('query.users', User.all)
Read more about prepared data.
1. 3. Kick Off
This step is where we choose what kind of request we want to run. Here are the three available options:
valid?(document)
It checks if the given document
is valid by running only the Organizing step.
If no errors were added, then the document is considered valid.
compile(document, compress: true)
It is similar to the above, but it generates a cached version of the request, which could be used to run multiple requests that skip the Organizing step.
- Arguments:
compress
true
- Marks if the return value should be compressed usingZlib#deflate
.
Important This is an experimental feature. More will be added to it soon.
execute(document, **settings)
It will run the request completely till it returns the result.
- Arguments:
operation_name
nil
- The name of the operation for logging purposes.args
/variables
{}
- The list of varaibles of the request.origin
nil
- An optional object from where the request originated. Usually mapped to a Controller or Channel.as
default_response_format
- The expected output format.
One of:string
,object
,json
,hash
.hash
nil
- The cache key of a cached request.compiled
false
- Indicates whether the incoming document is compiled.data_for
nil
- A shortcut for defining a series of prepared data.
1. 4. Parse and Run
This step is where the document will be parsed by the GQLParser#parse_execution
,
or fetched from the cache, and the proper runner will be called.
Note During this step, the schema will be validated (if it hasn’t been yet), which causes the first request to be slower than the others.
Variables
Variables, as defined by the GraphQL Spec, allow operations to be parameterized, maximize reuse, and avoid costly string building in clients. Here is an example on how to use variables:
query($active: Boolean!, $order: [SortingInput!]!) {
users(active: $active, order: $order) { id email }
}
request.execute('↑', variables: { active: true, order: [
{ field: 'email', direction: 'ASC' },
]})
Origin
The origin allows you to access the external world from where the request began without passing that into the context. This is preferable because the request will know how to deal with it in special cases properly.
You can also use the aliases request.controller
and request.channel
.
Strategy
A Strategy
class is actually the one responsible for running the following steps.
The request will look for the highest-ranked strategy from
request_strategies
that can_resolve?
itself.
Read more about strategies.
2. Organizing
This step will traverse through the parsed document and ensure everything that can actually be resolved from it.
Exceptions that may happen in this phase will be added to the errors
and properly tagged with extensions: { stage: "organize" }
.
This process involves:
Componentizing
Each operation (query, mutation, and subscription), field, fragment, and spread in your documents will be assigned to its own request component.
Validating
Check if the component is valid, which means different things for different components. Some examples: Does the spread point to an existing fragment? Does the type of the fragment exist in the schema? Does the return type has the request fields? Do the provided variables and fields’ arguments are equivalent?
If validation fails, the field will be marked as unresolvable
, and you will
be able to resolve the problem by addressing the issue reported.
Read more about components.
Listeners and Events
Collect all the events from the fields and keep track of what type of events are being listened to within the document.
3. Preparing
This step is the one-and-only opportunity for fields to collect data before they are actually resolved. This stage is the best middle ground where we know everything about what has been requested, and it can gather resources to fulfill the response. This is also the phase where mutations perform their actions.
Exceptions that may happen in this phase will be added to the errors
and properly tagged with extensions: { stage: "prepare" }
.
Data Stack
This step initializes an internal request context which represents a stack of the
underlying data of each field. Such s stack is called resolver
, and you can do
things like getting the current
, the parent
, and ancestors
.
Read more about the request data stack.
Data
In this step, we properly assign data to fields by either prepared data
or by the result of the prepare
(before_resolve
) event.
Performing
After getting the data above, a mutation field
will have its opportunity to be performed, which triggers a perform
event.
The returning value of the mutation will replace the one collected before.
4. Resolving
This is the final step of the process, where the document is traversed one last time, and values are added to the response in their proper format.
Exceptions that may happen in this phase will be added to the errors
and properly tagged with extensions: { stage: "resolve" }
.
Here is the list of ways a field can be resolved by order of precedence:
Array results will be resolved per field per item of the array in a
items
*fields
form.
4. 1. Resolver Callback
If the field has a resolve
callback, it will be called.
Inside the callback, you can access prepared_data
to get any prepared data assigned
to the field.
4. 2. Next Prepared Data
If data has been prepared for the field using the request’s prepared data, then it will return the next value.
The following options are ignored when the field is an entry point.
4. 3. Read from Hash
If the current value in the data stack is a Hash
, then it will attempt
to get a key with the method_name
or with the field’s
gql_name
.
4. 4. Call Method
As its last resource, it will try to call the method_name
from the current value in the data stack.
Quick Reference
Here is a quick reference for the resolve precedence:
# 1. Resolver Callback
field.resolver # Proc or Method
# 2. Prepared Data
request.prepared_data_for('User.id').next
# 3. Read from Hash
if resolver.current_value.is_a?(Hash)
resolver.current_value[:id] || resolver.current_value['id']
end
# 4. Call Method
resolver.current_value.id
Extras
Here is some extra information about requests:
Extensions
You can use the request.extensions
Hash
object to add data to the response
as you see fit. No restrictions nor validations will be applied to this portion
of the response.
request.extensions[:something] = 'Any value'
{
"data": { "..." },
"extensions": { "something": "Any value" }
}
Memo
If you need to keep data between fields and resolvers, you can add it to the operation’s memo.
The memo is a mutable Hash
that is isolated by operation, and any data added to it will
be discarded after the request has been completed.
operation.memo[:counter] ||= 0
operation.memo[:counter] += 1
Event
The Request::Event
instance is the object that is assigned to all classes after
they have been instantiated for resolving method-based callbacks.
Here is a quick reference of all the request-specific things you can get from the events:
# app/graphql/app_schema.rb
# Works with delegate_missing_to :event
def welcome
context # The request context
current # The current value in the data stack
current_value # Same as above
errors # The request errors
extensions # The request extensions
field # The current field being resolved
index # The index of the current array element
memo # The operation memo
operation # The operation component being resolved
prepared_data # The prepared data of a field, when provided
request # The request itself
resolver # The request data stack
schema # The schema of the request
strategy # The strategy of the request
subscription_provider # The subscription provider of the schema
argument(name) # Get the value of an argument sent to the field
arg(name) # Same as above
# Anything else will be attempted to be called from the `current_value`
something_else
end
Read more about events.
Event Types
Here is a list of all events that can happen during a request and when they will be triggered:
request
- When a
Strategy
is initiated to resolve a request. query
- When a
query
operation started to be organized. mutation
- When a
mutation
operation started to be organized. subscription
- When a
subscription
operation started to be organized. attach
- When a directive has been attached to a component.
authorize
- When a field is being organized, to check for authorization.
organized
- When a component has been successfully organized.
prepare
/before_resolve
- When a field is preparing data.
Runs in reverse order. perform
- When a mutation is performing its actions.
prepared
- When a component has been successfully prepared, and also performed in case of a mutation.
resolve
- When the value of a field is being resolved.
subscribed
- When a subscription was resolved and successfully added to the provider.
finalize
/after_resolve
- When a component has been successfully resolved.
Read more about events.
Components
All request components inherit from the Component
class, which has some useful
methods that you can use during your callbacks:
invalid?
- Checks if the component was marked as invalid.
skipped?
- Checks if the component should be skipped.
unresolvable?
- Same as
invalid? || skipped?
. broadcastable?
- Checks if the component has no broadcasting restrictions.
invalidate!(type = true)
- Marks the component as invalid, providing an optional type for the reason of the invalidation.
skip!
- Marks that the component should be skipped.
Here is a list of request components and their respective purposes:
Field
It represents each of the requested fields in the request. You can check things like
entry_point?
and mutation?
. You can also get the gql_name
, which
is either the alias or the field name.
Read more about fields.
Fragment
It represents a fragment added to the document which was actually initiated because a
spread is associated with it. It stores things like the used_variables
and used_fragments
.
Since fragments do not belong to a specific operation, their operation
value will be
dynamically associated with the one from the spread that is using itself.
Read more about fragments.
Operation
The base class for all types of operations. It stores things like the memo
,
the used_variables
, and the used_fragments
.
Each operation is a child class of this component, as in: Operation::Query < Operation
,
Operation::Mutation < Operation
, and Operation::Subscription < Operation
Calling kind
will always return operation
, but you can call query?
or similar methods
to identify the right type.
operation.used_variables # A set of used variables
operation.query? # Checks if it is a query operation
Spread
It represents a spread added to the document. You can check things like inline?
and
you can also access the fragment
associated with the spread, if any.
Read more about spreads.
Typename
The typename is a special type of field. Its only purpose is to return a plain string with the current GraphQL type within the scope to which it was requested. For example:
:001 > GraphQL::AppSchema.execute('{ __typename }')
=> # { "data" => { "__typename" => "_Query" } }
It is handy when working with interfaces and unions because it returns the actual type instead of these intermediate types.
Read more about the typename.