🌟 Enterprise Feature 🌟 This feature is bundled with GraphQL-Enterprise.
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::Changeset::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:
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…
field(...)
, including the same configurations you’d use in a type definition (see GraphQL::Schema::Field#initialize
). The definition given here will override the previous definition (if there was one) whenever this Changeset applies.remove_field(field_name)
, where field_name
is the name given to field(...)
(usually an underscore-cased symbol)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.
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…
argument(...)
, passing the same configurations you’d usually pass to argument(...)
(see GraphQL::Schema::Argument#initialize
). The redefined argument will override any previous definitions whenever this Changeset is active.remove_argument(argument_name)
, where argument_name
is the name given to field(...)
(usually an underscore-cased symbol)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.
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…
value(...)
, passing the same configurations you’d usually pass to value(...)
in an enum type (see GraphQL::Schema::Enum.value
). The configuration given here will override previous configurations whenever this Changeset applies.remove_value(name)
, where name
is the name given to value(...)
(an all-caps string)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.
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…
possible_types(*object_types)
, passing one or more object type classes. The given types will be added to the union’s set of possible types whenever this Changeset is active.remove_possible_types(*object_types)
, passing one or more object type classesWhen a possible type is removed, it will not be associated with the union type in introspection queries or schema dumps.
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…
implements(*interface_types)
, passing one or more interface type modules. This will add the interface and its fields to the object whenever this Changeset is active.remove_implements(*interface_types)
, passing one or more interface type modulesWhen 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.)
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.
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.