⚠ New Class-Based API ⚠

This describes a new API in v1.8.0. Check the upgrade notes for more info.

Class-based API

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.

You can get an overview of this new feature:

And learn about the APIs:

Rationale & Goals

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.

Compatibility & Migration overview

Parts of your schema can be converted one-by-one, so you can convert definitions gradually.

Classes

In general, each .define { ... } block will be converted to a class.

See sections below for specific information about each schema definition class.

Type Instances

The previous GraphQL::{X}Type objects are still used under the hood. Each of the new GraphQL::Schema::{X} classes implements a few methods:

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.

List Types and Non-Null Types

Previously, list types were expressed with types[T] and non-null types were expressed with !T. Now:

In legacy-style classes, you may also use plain Ruby methods to create list and non-null types:

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

Connection fields & types

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

Resolve function compatibility

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.

Upgrader

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 open an issue.

Using the Default Upgrade Task

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:

Writing a Custom Upgrade Task

You 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:

Keep 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)

Writing a custom transformer

Objects in the transform pipeline may be:

The 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)

Built-in transformers

Follow links to the API doc to read the source of each transform:

Type transforms (GraphQL::Upgrader::Member::DEFAULT_TYPE_TRANSFORMS):

Field transforms (GraphQL::Upgrader::Member::DEFAULT_FIELD_TRANSFORMS):

Clean-up transforms (GraphQL::Upgrader::Member::DEFAULT_CLEAN_UP_TRANSFORMS):

Roadmap

Here is a working plan for rolling out this feature:

Common Type Configurations

Some configurations are used for all types described below:

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).