Resolve "[graphql] Pull together significant changes to communicate"
What does this MR do and why?
Details many of the changes made when upgrading the GraphQL Ruby gem from 1.11.x
to 1.13.x
. The reason for so many changes is that we started using the GraphQL - Interpreter
Related to #363130 (closed)
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.
Minor Points of Interest
These are things that are minor and not a big deal, and most of them will cause an error during a spec if you do something that is not supported.
-
resolve
procs are no longer supported by the gem. example MRFor example, this is no longer supported
field :project, Types::ProjectType, description: 'The project the snippet is associated with', null: true, authorize: :read_project resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find }
Instead, use a method for resolving:
field :project, Types::ProjectType, description: 'The project the snippet is associated with', null: true, authorize: :read_project def project Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find end
-
We should always use the newer standard types, such as
GraphQL::Types::Int
, instead ofGraphQL::INT_TYPE
. example MR. A cop was created to enforce this. -
default values on arguments should have have the same type. For example, if an argument is of type
GraphQL::Types::String
, then thedefault_value
should also be of typeGraphQL::Types::String
. example MRargument :ref, GraphQL::Types::String, required: false, required: false, default_value: 'some value'
-
to_graphql
is deprecated and will be removed in2.0
. Do not use. Most information you need is now on the actual field, such as in this MR -
use
.to_type_signature
instead ofto_graphql
when you need the type name. example MR -
most errors are no longer raised as exceptions, but are bubbled up from the framework. example MR
When testing resolvers, instead of
expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
use
expect_graphql_error_to_be_created
insteadexpect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do result end
-
When creating a field extension, subclass from
GraphQL::Schema::FieldExtension
instead ofGraphQL::Schema::Field::ConnectionExtension
. You can otherwise get duplicate connection methods defined. example MR -
Use our
empty_schema
fromGraphqlHelpers
instead of trying to create your own. example MR -
use
query_double(schema: nil)
fromGraphqlHelpers
instead ofdouble('query', schema: nil)
. example MR -
when exceptions are raised in the lower levels of the code/gem, we now add backtrace information to the error message, which can help track down some tricky bugs. MR
-
calls to
GraphQL::ObjectType.accepts_definitions
andGraphQL::Define.assign_metadata_key
are no longer supported and have been removed. MR -
the gem changed how input names were calculated. Having
graphql_name
called after any arguments were defined could cause naming issues. So we just standardized on putting it at the top. A cop was created to enforce it. See [graphql] Move graphql_name call to beginning of class and DuplicateNamesError when upgrading to 1.13 -
Gitlab::Graphql::Present
was converted to a field extension, instead of our old instrumentation. MR -
Replaced
calls-gitaly
instrumentation with a field extension, MR
Important
-
if you use a
prepare
proc in anInput
type, such asTypes::Boards::BoardIssueInputType
, it should return the same type. In other words, don't return a.to_h
. This may change in a future update, but for now it doesn't work: See Getting undefined method `arguments' in extract_deprecated_arguments. example MRThe
.to_h
is not supported as it does not return a value of the original typeargument :not, NegatedBoardIssueInputType, required: false, prepare: ->(negated_args, ctx) { negated_args.to_h }, description: 'List of negated arguments.'
-
There can be instances where a spec will fail because the class is not loaded correctly. The errors won't quite make sense. It relates to the circular dependencies problem and Adding field with resolver on a Type causes "Can't determine the return type " error on a different Type. The only way we've found to fix this right now is quoting the class name in the spec. For example, changing
RSpec.describe Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver do
into
RSpec.describe 'Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver' do
See here for some discussion
-
possible cyclic dependencies, which main be resolved once we've upgraded to
2.x
. See Adding field with resolver on a Type causes "Can't determine the return type " error on a different Type and Fix unresolved name due to cyclic definition# Normally this wouldn't be needed and we could use # type Types::IssueType.connection_type, null: true # in a resolver. However we can end up with cyclic definitions, # which can result in errors like # NameError: uninitialized constant Resolvers::GroupIssuesResolver # # Now we would use # type "Types::IssueConnection", null: true # which gives a delayed resolution, and the proper connection type. # See app/graphql/resolvers/base_issues_resolver.rb # Reference: https://github.com/rmosolgo/graphql-ruby/issues/3974#issuecomment-1084444214
-
FindClosest
was removed as it relied heavily on the internal node structure. MR Instead:- use the query context to pass information to lower-parts of the evaluation tree (using
scoped_set!
) - use
parent
to examine the immediate parent of the current node (no traversal is possible, but you can see through connections) - use
context.namespace(:interpreter)[:current_path]
to see the current path. - use statically available information on the objects
- use the query context to pass information to lower-parts of the evaluation tree (using
-
When testing resolvers using
GraphqlHelpers#resolve
, arguments for the resolver can be handled two ways.-
95% of the resolver specs use arguments that are ruby objects, as opposed to when using the GraphQL API only strings and integers are used. This works fine in most cases.
-
If your resolver takes arguments that use a
prepare
proc, such as a resolver that accepts timeframe arguments (TimeFrameArguments
), you must pass thearg_style: :internal_prepared
parameter into theresolve
method. This tells the code to convert the arguments into strings and integers and pass them through regular argument handling, ensuring that theprepare
proc is called correctly. For example initerations_resolver_spec.rb
:def resolve_group_iterations(args = {}, obj = group, context = { current_user: current_user }) resolve(described_class, obj: obj, args: args, ctx: context, arg_style: :internal_prepared) end
One additional caveat is that if you are passing enums as a resolver argument, you must use the external representation of the enum, rather than the internal. For example:
# good resolve_group_iterations({ search: search, in: ['CADENCE_TITLE'] }) # bad resolve_group_iterations({ search: search, in: [:cadence_title] })
The use of
:internal_prepared
was added as a bridge for the GraphQL gem upgrade. Testing resolvers directly will be removed eventually via issue #363121, and writing unit tests for resolvers/mutations is already deprecated
-