🌟 Enterprise Feature 🌟 This feature is bundled with GraphQL-Enterprise.

Defining Changesets

After installing Changeset integrations in your schema, you can create Changesets which modify parts of the schema. Changesets extend GraphQL::Enterprise::Changeset and include a release and some modifies ... configurations.

For example, this Changeset marks the old Recipe.flag field as deprecated:

# app/graphql/changesets/deprecate_recipe_flag.rb
class Changesets::DeprecateRecipeFlag < GraphQL::Enterprise::Changeset
  release "2020-12-01"
  modifies Types::Recipe do
    field :flag, Types::RecipeFlag, null: false, deprecation_reason: "Recipes now have multiple flags, use `flags` instead."
  end
end

Then this Changeset removes Recipe.flag entirely:

# app/graphql/changesets/remove_recipe_flag.rb
class Changesets::RemoveRecipeFlag < GraphQL::Enterprise::Changeset
  release "2021-03-01"
  modifies Types::Recipe do
    remove_field :flag
  end
end

Additionally, the Changesets must be added to the schema (see the Releases guide):

class MyAppSchema < GraphQL::Schema
  # ...
  use GraphQL::Enterprise::Changesets::Release, changesets: [
    Changesets::DeprecateRecipeFlag,
    Changesets::RemoveRecipeFlag,
  ]
end

Although the changesets above have one modification each, a changeset may have any number of modifications in it.

See below for the different kind of modifications you can make in a changeset:

Fields

In a Changeset, you can add, redefine, or remove fields that belong to object types, interface types, or resolvers. First, use modifies ... do ... end, naming the owner of the field:

class Changesets::RecipeMigration < GraphQL::Enterprise::Changeset
  modifies Types::Recipe do
    # modify `Recipe`'s fields here
  end
end

Then…

When a field is removed, queries that request that field will be invalid, unless the client has requested a previous API version where the field is still available.

Arguments

In a Changeset, you can add, redefine, or remove arguments that belong to fields, input objects, or resolvers. Use modifies to select the argument owner, for example:

class Changesets::FilterMigration < GraphQL::Enterprise::Changeset
  modifies Types::IngredientsFilter do
    # modify input object arguments here
  end
  # ...

When versioning field arguments, use a second modifies(field_name) { ... } call to select the field to modify:

  # ...
  modifies Types::Query do
    modifies :ingredients do
      # modify the arguments of `Query.ingredients(...)` here
    end
  end
end

Then…

When arguments are removed, the schema will reject any queries which use them unless the client has requested a previous API version where the argument is still allowed.

Enum Values

In a Changeset, you can add, redefine, or remove enum values. First, use modifies ... do ... end, naming the enum type:

class Changesets::RecipeFlagMigration < GraphQL::Enterprise::Changeset
  modifies Types::RecipeFlag do
    # Modify `RecipeFlag`'s values here
  end
end

Then…

When enum values are removed, they won’t be accepted as input and they won’t be allowed as return values from fields unless the client has requested a previous API version where those values are still allowed.

Unions

In a Changeset, you can add to or remove from a union’s possible types. First, use modifies ..., naming the union type:

class Changesets::MigrateLegacyCookingTechniques < GraphQL::Enterprise::Changeset
  modifies Types::CookingTechnique do
    # change the possible_types of the `CookingTechnique` union here
  end
end

Then…

When a possible type is removed, it will not be associated with the union type in introspection queries or schema dumps.

Interfaces

In a Changeset, you can add to or remove from an object type’s interface definitions. First, use modifies ..., naming the object type:

class Changesets::ModifyImplements < GraphQL::Enterprise::Changeset
  modifies Types::Ingredient do
    # change `Ingredient`'s interface implementations here
  end
end

Then…

When an interface implementation is removed, then the interface will not be associated with the object in introspection queries or schema dumps. Also, any fields inherited from the interface will be hidden from clients. (If the object defines the field itself, it will still be visible.)

Types

Using Changesets, it’s possible to define a new type using the same name as an old type. (Only one type per name is allowed for each query, but different queries can use different types for the same name.)

First, to define two types with the same name, make two different type definitions. One of them will have to use graphql_name(...) to specify the conflicting type name. For example, to migrate an enum type to an object type, define two types:

# app/graphql/types/legacy_recipe_flag.rb

# In the old version of the schema, "recipe flags" were limited to defined set of values.
# This enum was renamed from `Types::RecipeFlag`, then `graphql_name("RecipeFlag")`
# was added for GraphQL.
class Types::LegacyRecipeFlag < Types::BaseEnum
  graphql_name "RecipeFlag"
  # ...
end
# app/graphql/types/recipe_flag.rb

# But in the new schema, each flag is a full-fledge object with fields of its own
class Types::RecipeFlag < Types::BaseObject
  field :name, String, null: false
  field :is_vegetarian, Boolean, null: false
  # ...
end

Then, add or update fields or arguments to use the new type instead of the old one. For example:

class Changesets::MigrateRecipeFlagToObject < GraphQL::Enterprise::Changeset
  modifies Types::Recipe do
    # in types/recipe.rb, this is defined with `field :flags, [Types::LegacyRecipeFlag]`
    # Here, update the field to use the _object_ instead:
    update_field :flags, [Types::RecipeFlag]
  end
end

With that Changeset, Recipe.flags will return an object type instead of an enum type. Clients requesting older versions will still receive enum values from that field.

The resolver will probably need an update, too, for example:

class Types::Recipe < Types::BaseObject
  # Here's the original definition, which is modified by `MigrateRecipeFlagToObject`:
  field :flags, [Types::LegacyRecipeFlag], null: false

  def flags
    all_flag_objects = object.flag_objects
    if Changesets::MigrateRecipeFlagToObject.active?(context)
      all_flag_objects
    else
      # Convert this to enum values, for legacy behavior:
      all_flag_objects.map { |f| f.name.upcase }
    end
  end
end

That way, legacy clients will continue to receive enum values while new clients will receive objects.

Runtime

While a query is running, you can check if a changeset applies by using its .active?(context) method. For example:

class Types::Recipe
  field :flag, Types::RecipeFlag, null: true

  def flag
    # Check if this changeset applies to the current request:
    if Changesets::DeprecateRecipeFlag.active?(context)
      Stats.count(:deprecated_recipe_flag, context[:viewer])
    end
    # ...
  end
end

Besides observability, you can use a runtime check when a resolver needs to pick a different behavior depending on the API version.

After defining a changeset, add it to the schema to release it.