graphql_includable
Eager-load graphql-ruby query data using Rails models
When resolving a GraphQL query with this model at the root, graphql_includable will eager-load all queried models using ActiveRecord::QueryMethods::includes.
Usage
- Define your relationships as ActiveRecord associations.
class Apple < ActiveRecord::Base
belongs_to :tree
end
class Tree < ActiveRecord::Base
has_many :apples
end- Annotated your GraphQL fields with
includes
AppleType = GraphQL::ObjectType.define do
name "Apple"
field :tree, !types[!TreeType], includes :tree
end
TreeType = GraphQL::ObjectType.define do
name "Tree"
field :apples, !types[!AppleType], includes :apples
end- Call
GraphQLIncludable.includeswhen resolving the query, passing in the query context.
BaseQuery = GraphQL::ObjectType.define do
field :tree, TreeType do
argument :id, !types.ID
resolve -> (obj, args, ctx) {
includes = GraphQLIncludable.includes(ctx)
Tree.includes(includes).find_by(args.to_h)
}
end
endWhen resolving a query for tree.apples, the association apples will be preloaded on Tree because of the includes annotation on the field.
Extra includes
includes can take a lambda that can be used to build up any includes pattern.
For example, let's say a field needs to include a grandchild association on the model.
class Building < ActiveRecord::Base
has_one :location
class Location < ActiveRecord::Base
has_one :address
end
class Address < ActiveRecord::Base
end
BuildingType = GraphQL::ObjectType.define do
field :address, !AddressType do
includes ->() {
path(:location) {
path(:address)
}
}
resolve ->(building, args, ctx) {
building.location.address
}
end
endYou can also include associations where child includes are not chained on.
class Building < ActiveRecord::Base
has_one :location
class Location < ActiveRecord::Base
has_one :address
has_one :neighborhood
end
class Address < ActiveRecord::Base
end
class Neighborhood < ActiveRecord::Base
end
AddressType = GraphQL::ObjectType.define do
field :street, !types.String
field :city, !types.String
end
AddressWithNeighborhoodType = GraphQL::ObjectType.define do
field :address, !AddressType
field :neighborhood, !types.String
end
BuildingType = GraphQL::ObjectType.define do
field :address, !AddressWithNeighborhoodType do
includes ->() {
path(:location) {
path(:address) # Deeper includes would be added here { building: { location: [:neighborhood, { address: ... }] } }
sibling_path(:neighborhood)
}
}
resolve ->(building, args, ctx) {
{
address: building.location.address,
neighborhood: building.location.neighborhood.name
}
}
end
endConditional includes generation
includes can take a lambda which can check the args and ctx to decide what to include.
field :conditional_apples do
argument :kind, !types.String
includes ->(args, ctx) {
return :red_delicious_apples if args[:kind] == 'Red Delicious'
return :pink_pearl_apples if args[:kind] == 'Pink Pearls'
:apples # fall back to including all apples
}
resolve ->(obj, args, ctx) {
return tree.red_delicious_apples if args[:kind] == 'Red Delicious'
return tree.pink_pearl_apples if args[:kind] == 'Pink Pearls'
tree.apples
}
endConnection Support
Edges and node associations will be added to the includes pattern. Asking for only nodes will not include the edge association.
As a simple example where a connection just has `nodes.
connection :surveys do
type SurveyType.define_connection
argument :sent, types.Boolean
includes ->(args, ctx) {
nodes(:surveys) # When querying `nodes`, include association `surveys`
}
resolve ->(obj, inputs, ctx) {
obj.surveys # Eagerly loaded
}
endWhenever your connection is backed by join-table modelling, you can use a custom connection type that will efficiently fetch edges and or nodes.
class Survey < ActiveRecord::Base
has_many :survey_listings
has_many :listings, through: :survey_listings
end
class SurveyListing < ActiveRecord::Base
belongs_to :listing
end
class Listing < ActiveRecord::Base
end
SurveyType = GraphQL::ObjectType.define do
connection :listings, ListingType.define_connection_with_fetched_edge(edge_type: SurveyListingEdgeType) do
# Tell fetched-edge-connection how to fetch `nodes` from Survey, `edges` from Survey
# and how to get from a SurveyListing to the `node` Listing
connection_properties(nodes: :listings, edges: :survey_listings, edge_to_node: :listing)
includes ->() {
nodes(:listings) # include :listings when querying nodes directly
edges do
path(:survey_listings) # include :survey_listings when querying edges directly
node(:listing) # include SurveyListing's :listing association when querying edges then node
end
}
end
end
GraphQLSchema = GraphQL::Schema.define do
query BaseQuery
mutation BaseMutation
# Add this instrumentation
instrument(:field, GraphQLIncludable::Relay::Instrumentation.new)
endExamples of generated includes:
query {
surveys {
listings {
nodes {
id
}
}
}
}
includes([:listings])
query {
surveys {
listings {
edges {
sent
}
}
}
}
includes([:survey_listings])
query {
surveys {
listings {
edges {
sent
node {
id
}
}
}
}
}
includes({ survey_listings: [:listing] })Migrating from 0.4 to 0.5
With version 0.5 a new, more powerful GraphQLIncludable API has been introduced. This is currently namespaced behind GraphQLIncludable::New and
any associated attributes are prefixed with new_, for example new_includes vs the old API's includes.
Namespacing this API allows applications to run the old and new APIs side by side, there is no need for a big bang migration.
Version 0.5 is the last verion to support the old API.
You should migrate to version 0.5 before any future versions as GraphQLIncludable::New namespace and, more critically, the new_ prefix will
be dropped from new_includes, interferring with and breaking the old includes_from_graphql API.
In order to simplify the implementation and improve connection support, ActiveRecord introspection was removed. This means your GraphQL fields
now require explicit annotation that they are to be evaluated for inclusion.
- For all fields that use ActiveRecord associations add a
new_includesannotation. - Add the following instrumentation to your query for Connection support
instrument(:field, GraphQLIncludable::New::Relay::Instrumentation.new)
- Start replacing calls to
Model.includes_from_graphqlwithYou can control at which point in the GraphQL query to start generating includes fromModel.includes(GraphQLIncludable::New.includes(ctx))
SearchType = GraphQL::ObjectType.define do name 'Search' field :count, !types.Int field :offset, !types.Int field :results, !types[ResultType] end SearchField = GraphQL::Field.define do type SearchType argument ... resolve ->(obj, args, ctx) { includes = GraphQLIncludable::New.includes(ctx, starting_at: :results) Result.includes(includes).where(...) } end
For example:
AppleType = GraphQL::ObjectType.define do
name "Apple"
field :tree, !types[!TreeType]
end
TreeType = GraphQL::ObjectType.define do
name "Tree"
field :apples, !types[!AppleType]
end
BaseQuery = GraphQL::ObjectType.define do
field :tree, TreeType do
argument :id, !types.ID
resolve -> (obj, args, ctx) {
# Old API - generates includes(:apples)
trees = Tree.includes_from_graphql(ctx).find_by(args.to_h) # No N+1 problems
# New API - generates includes()
includes = GraphQLIncludable::New.includes(ctx)
Tree.includes(includes).find_by(args.to_h) # Would create N+1 problems
}
end
endBy annotating the types with new_includes, both the old and new API will work side by side.
AppleType = GraphQL::ObjectType.define do
name "Apple"
field :tree, !types[!TreeType], new_includes: :tree
end
TreeType = GraphQL::ObjectType.define do
name "Tree"
field :apples, !types[!AppleType], new_includes: :apples
end
BaseQuery = GraphQL::ObjectType.define do
field :tree, TreeType do
argument :id, !types.ID
resolve -> (obj, args, ctx) {
# Old API - generates includes(:apples)
trees = Tree.includes_from_graphql(ctx).find_by(args.to_h) # No N+1 problems
# New API - generates includes(:apples)
includes = GraphQLIncludable::New.includes(ctx)
Tree.includes(includes).find_by(args.to_h) # No N+1 problems
}
end
endMigrating from 0.5 to 1.0
Ensure you have migrated to the new API by following the above guidelines before continuing
- Remove any
includes:overrides leftover from the old API. You may have already done this as part of the 0.5 release. - Rename any
new_includesfield annotations withincludes - Replace calls to
new_define_connection_with_fetched_edgewithdefine_connection_with_fetched_edge - Replace schema instrumentation
instrument(:field, GraphQLIncludable::New::Relay::Instrumentation.new)withinstrument(:field, GraphQLIncludable::Relay::Instrumentation.new) - Replace includes generation statements
GraphQLIncludable::New.includeswithGraphQLIncludable.includes - Remove
include GraphQLIncludable::Concernfrom your Active Record models
AppleType = GraphQL::ObjectType.define do
name "Apple"
field :tree, !types[!TreeType], includes: :tree
end
TreeType = GraphQL::ObjectType.define do
name "Tree"
field :apples, !types[!AppleType], includes: :apples
end
BaseQuery = GraphQL::ObjectType.define do
field :tree, TreeType do
argument :id, !types.ID
resolve -> (obj, args, ctx) {
includes = GraphQLIncludable.includes(ctx)
Tree.includes(includes).find_by(args.to_h)
}
end
end
GraphQLSchema = GraphQL::Schema.define do
query BaseQuery
mutation BaseMutation
instrument(:field, GraphQLIncludable::Relay::Instrumentation.new)
end