In GraphQL 1.8
+, you can use Ruby classes to build your schema. You can mix class-style and .define
-style type definitions in a schema.
The .define
DSL is deprecated and will be removed at version 2.0.
You can get an overview of this new feature:
And learn about the APIs:
This new API aims to improve the “getting started” experience and the schema customization experience by replacing GraphQL-Ruby-specific DSLs with familiar Ruby semantics (classes and methods).
Additionally, this new API must be cross-compatible with the current schema definition API so that it can be adopted bit-by-bit.
Parts of your schema can be converted one-by-one, so you can convert definitions gradually.
In general, each .define { ... }
block will be converted to a class.
GraphQL::{X}Type
, classes inherit from GraphQL::Schema::{X}
. For example, instead of GraphQL::ObjectType.define { ... }
, a definition is made by extending GraphQL::Schema::Object
ApplicationController
in Rails, see Customizing Definitions).See sections below for specific information about each schema definition class.
Keep in mind that class based Schemas will be initialized at execution time instead of application boot, depending on the size of your schema, this could result in request timeouts for your users after your application restarts. For a workaround please check https://github.com/rmosolgo/graphql-ruby/issues/2034
The previous GraphQL::{X}Type
objects are still used under the hood. Each of the new GraphQL::Schema::{X}
classes implements a few methods:
.to_graphql
: creates a new instance of GraphQL::{X}Type
.graphql_definition
: returns a cached instance of GraphQL::{X}Type
If you have custom code which breaks on new-style definitions, try calling .graphql_definition
to get the underlying type object.
As described below, .to_graphql
can be overridden to customize the type system.
Previously, list types were expressed with types[T]
and non-null types were expressed with !T
. Now:
[T]
, for example, field :owners, [Types::UserType]
[Types::UserType]
becomes [User!]
, null: true
to the array: [Types::UserType, null: true]
becomes [User]
(the list may include nil
)null:
or required:
field
takes a keyword null:
. null: true
means the field is nullable, null: false
means the field is non-null (equivalent to !
)argument
takes a keyword required:
. required: true
means the argument is non-null (equivalent to !
), required: false
means that the argument is nullableIn legacy-style classes, you may also use plain Ruby methods to create list and non-null types:
#to_non_null_type
converts a type to a non-null variant (ie, T.to_non_null_type
is equivalent to !T
)#to_list_type
converts a type to a list variant (ie, T.to_list_type
is equivalent to types[T]
)The !
method has been removed to avoid ambiguity with the built-in logical operator and related foot-gunning.
For compatibility, you may wish to backport !
to class-based type definitions. You have two options:
A refinement, activated in file scope or class/module scope:
# Enable `!` method in this scope
using GraphQL::DeprecatedDSL
A monkeypatch, activated in global scope:
# Enable `!` everywhere
GraphQL::DeprecatedDSL.activate
There is no connection(...)
method. Instead, connection fields are inferred from the type name.
If the type name ends in Connection
, the field is treated as a connection field.
This default may be overridden by passing a connection: true
or connection: false
keyword.
For example:
# This will be treated as a connection, since the type name ends in "Connection"
field :projects, Types::ProjectType.connection_type
If you define a type with a class, you can use existing GraphQL-Ruby resolve functions with that class, for example:
# Using a Proc literal or #call-able
field :something, ... resolve: ->(obj, args, ctx) { ... }
# Using a predefined field
field :do_something, field: Mutations::DoSomething.field
# Using a GraphQL::Function
field :something, function: Functions::Something.new
When using these resolution implementations, they will be called with the same (obj, args, ctx)
parameters as before.
1.8
includes an auto-upgrader for transforming Ruby files from the .define
-based syntax to class
-based syntax. The upgrader is a pipeline of sequential transform operations. It ships with default pipelines, but you may customize the upgrade process by replacing the built-in pipelines with a custom ones.
The upgrader has an additional dependency, parser
, which you must add to your project manually (for example, by adding to your Gemfile
).
Remember that your project may be transformed one file at a time because the two syntaxes are compatible. This way, you can convert a few files and run your tests to identify outstanding issues, and continue working incrementally.
This transformation may not be perfect, but it should cover the most common cases. If you want to ask a question or report a bug, please <a href=’https://github.com/rmosolgo/graphql-ruby/issues/new?title=”Upgrader question/bug report”&body=”Please share: the source code you’re trying to transform, the output you got from the transformer, and the output you want to get from the transformer.”‘>open an issue</a>.
The upgrader ships with rake tasks, included as a railtie (source). The railtie will be automatically installed by your Rails app, and it provides the following tasks:
graphql:upgrade:schema[path/to/schema.rb]
: upgrade the Schema filegraphql:upgrade:member[path/to/some/type.rb]
: upgrade a type definition (object, interface, union, etc)graphql:upgrade[app/graphql/**/*]
: run the member
upgrade on files which have a suffix of _(type|interface|enum|union).rb
graphql:upgrade:create_base_objects[path/to/graphql/]
: add base classes to your projectYou might write a custom task because:
To write a custom task, you can write a rake task (or Ruby script) which uses the upgrader’s API directly.
Here’s the code to upgrade a type definition with the default transform pipeline:
# Read the original source code into a string
original_source = File.read("path/to/type.rb")
# Initialize an upgrader with the default transforms
upgrader = GraphQL::Upgrader::Member.new(original_source)
# Perform the transformation, get the transformed source code
transformed_source = upgrader.upgrade
# Update the source file with the new code
File.write("path/to/type.rb", transformed_source)
In this custom code, you can pass some keywords to GraphQL::Upgrader::Member.new
:
type_transforms:
Applied to the source code as a whole, applied firstfield_transforms:
Applied to each field/connection/argument definition (extracted from the source, transformed independently, then re-inserted)clean_up_transforms:
Applied to the source code as a whole, after the type and field transformsKeep in mind that these transforms are performed in sequence, so the text changes over time. If you want to transform the source text, use .unshift()
to add transforms to the beginning of the pipeline instead of the end.
For example, in script/graphql-upgrade
:
#!/usr/bin/env ruby
# @example Upgrade app/graphql/types/user_type.rb:
# script/graphql-upgrade app/graphql/types/user_type.rb
# Replace the default define-to-class transform with a custom one:
type_transforms = GraphQL::Upgrader::Member::DEFAULT_TYPE_TRANSFORMS.map { |t|
if t == GraphQL::Upgrader::TypeDefineToClassTransform
GraphQL::Upgrader::TypeDefineToClassTransform.new(base_class_pattern: "Platform::\\2s::Base")
else
t
end
}
# Add this transformer at the beginning of the list:
type_transforms.unshift(GraphQL::Upgrader::ConfigurationToKwargTransform.new(kwarg: "visibility"))
# run the upgrader
original_text = File.read(ARGV[0])
upgrader = GraphQL::Upgrader::Member.new(original_text, type_transforms: type_transforms)
transformed_text = upgrader.upgrade
File.write(filename, transformed_text)
Objects in the transform pipeline may be:
.new.apply(input_text)
and returns the transformed code.apply(input_text)
and returns the transformed codeThe library provides a GraphQL::Upgrader::Transform
base class with a few convenience methods. You can also customize the built-in transformers listed below.
For example, here’s a transform which rewrites type definitions from a model_type(model) do ... end
factory method to the class-based syntax:
# Create a custom transform for our `model_type` factory:
class ModelTypeToClassTransform < GraphQL::Upgrader::Transform
def initialize
# Find calls to the factory method, which have a type class inside
@find_pattern = /^( +)([a-zA-Z_0-9:]*) = model_type\(-> ?\{ ?:{0,2}([a-zA-Z_0-9:]*) ?\} ?\) do/
# Replace them with a class definition and a `model_name("...")` call:
@replace_pattern = "\\1class \\2 < Platform::Objects::Base\n\\1 model_name \"\\3\""
end
def apply(input_text)
# Run the substitution on the input text:
input_text.sub(@find_pattern, @replace_pattern)
end
end
# Add the class to the beginning of the pipeline
type_transforms.unshift(ModelTypeToClassTransform)
Follow links to the API doc to read the source of each transform:
Type transforms (GraphQL::Upgrader::Member::DEFAULT_TYPE_TRANSFORMS
):
GraphQL::Upgrader::Transform
base class, provides a normalize_type_expression
helperGraphQL::Upgrader::TypeDefineToClassTransform
turns .define
into class ...
with a regexp substitutionGraphQL::Upgrader::NameTransform
takes name "..."
and removes it if it’s redundant, or converts it to graphql_name "..."
GraphQL::Upgrader::InterfacesToImplementsTransform
turns interfaces [A, B...]
into implements(A)\nimplements(B)...
Field transforms (GraphQL::Upgrader::Member::DEFAULT_FIELD_TRANSFORMS
):
GraphQL::Upgrader::RemoveNewlinesTransform
removes newlines from field definitions to normalize themGraphQL::Upgrader::PositionalTypeArgTransform
moves type X
from the do ... end
block into a positional argument, to normalize the definitionGraphQL::Upgrader::ConfigurationToKwargTransform
moves a do ... end
configuration to a keyword argument. By default, this is used for property
and description
. You can add new instances of this transform to convert your custom DSL.GraphQL::Upgrader::PropertyToMethodTransform
turns property:
to method:
GraphQL::Upgrader::UnderscoreizeFieldNameTransform
converts field names to underscore-case. NOTE that this conversion may be wrong in the case of bodyHTML => body_html
. When you find it is wrong, manually revert it and preserve the camel-case field name.GraphQL::Upgrader::ResolveProcToMethodTransform
converts resolve -> { ... }
to def {field_name} ...
method definitionsGraphQL::Upgrader::UpdateMethodSignatureTransform
converts the type name to the new syntax, and adds null:
/required:
to the method signatureClean-up transforms (GraphQL::Upgrader::Member::DEFAULT_CLEAN_UP_TRANSFORMS
):
GraphQL::Upgrader::RemoveExcessWhitespaceTransform
removes redundant newlinesGraphQL::Upgrader::RemoveEmptyBlocksTransform
removes do end
with nothing inside themHere is a working plan for rolling out this feature:
Context
classesSchema#execute
method.define
: isolate it in its own module.define
Some configurations are used for all types described below:
graphql_name
overrides the type name. (The default value is the Ruby constant name, without any namespaces)description
provides a description for GraphQL introspection.For example:
class Types::TodoList < GraphQL::Schema::Object # or Scalar, Enum, Union, whatever
graphql_name "List" # Overrides the default of "TodoList"
description "Things to do (may have already been done)"
end
(Implemented in GraphQL::Schema::Member
).