A stitching client is constructed with many subgraph schemas, and must first compose them into one unified schema that can introspect and validate supergraph requests. Composition only happens once upon initialization.
When building a client, pass a locations hash with named definitions for each subgraph location:
client = GraphQL::Stitching::Client.new(locations: {
products: {
schema: GraphQL::Schema.from_definition(File.read("schemas/products.graphql")),
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
},
users: {
schema: GraphQL::Schema.from_definition(File.read("schemas/users.graphql")),
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
stitch: [{ field_name: "users", key: "id" }],
},
my_local: {
schema: MyLocalSchema,
},
})Location settings have top-level keys that specify arbitrary location name keywords, each of which provide:
-
schema:required, provides aGraphQL::Schemaclass for the location. This may be a class-based schema that inherits fromGraphQL::Schema, or built from SDL (Schema Definition Language) string usingGraphQL::Schema.from_definitionand mapped to a remote location. The provided schema is only used for type reference and does not require any real data resolvers (unless it's also used as the location's executable, see below). -
executable:, provides an executable resource to be called when delegating a request to this location, see documentation. Omitting the executable option will use the location's providedschemaas the executable resource. -
stitch:, an array of static configs used to dynamically apply@stitchdirectives to root fields while composing. Each config may specifyfield_name,key,arguments, andtype_name.
When building a client, you may pass composer_options to tune how it builds a supergraph. All settings are optional:
client = GraphQL::Stitching::Client.new(
composer_options: {
query_name: "Query",
mutation_name: "Mutation",
subscription_name: "Subscription",
visibility_profiles: nil, # ["public", "private", ...]
description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
root_entrypoints: {},
},
locations: {
# ...
}
)-
query_name:, the name of the root query type in the composed schema;Queryby default. The root query types from all location schemas will be merged into this type, regardless of their local names. -
mutation_name:, the name of the root mutation type in the composed schema;Mutationby default. The root mutation types from all location schemas will be merged into this type, regardless of their local names. -
subscription_name:, the name of the root subscription type in the composed schema;Subscriptionby default. The root subscription types from all location schemas will be merged into this type, regardless of their local names. -
visibility_profiles:, an array of visibility profiles that the supergraph responds to. -
description_merger:, a value merger function for merging element description strings from across locations. -
deprecation_merger:, a value merger function for merging element deprecation strings from across locations. -
default_value_merger:, a value merger function for merging argument default values from across locations. -
directive_kwarg_merger:, a value merger function for merging directive keyword arguments from across locations. -
root_entrypoints:, a hash of root field names mapped to their entrypoint locations, see overlapping root fields below.
Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element attribute is used. A value merger function may customize this process by selecting a different value or computing a new one:
join_values_merger = ->(values_by_location, info) { values_by_location.values.compact.join("\n") }
client = GraphQL::Stitching::Client.new(
composer_options: {
description_merger: join_values_merger,
deprecation_merger: join_values_merger,
default_value_merger: join_values_merger,
directive_kwarg_merger: join_values_merger,
},
)A merger function receives values_by_location and info arguments; these provide possible values keyed by location and info about where in the schema these values were encountered:
values_by_location = {
"users" => "A fabulous data type.",
"products" => "An excellent data type.",
}
info = {
type_name: "Product",
# field_name: ...,
# argument_name: ...,
# directive_name: ...,
}Composition is a nuanced process with a high potential for validation failures. While performing composition at runtime is fine in development mode, it becomes an unnecessary risk in production. It's much safer to compose your supergraph in development mode, cache the composition, and then rehydrate the supergraph from cache in production.
First, compose your supergraph in development mode and write it to file:
client = GraphQL::Stitching::Client.new(locations: {
products: {
schema: GraphQL::Schema.from_definition(File.read("schemas/products.graphql")),
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
},
users: {
schema: GraphQL::Schema.from_definition(File.read("schemas/users.graphql")),
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
},
my_local: {
schema: MyLocalSchema,
},
})
File.write("schemas/supergraph.graphql", client.supergraph.to_definition)Then in production, rehydrate the client using the cached supergraph and its production-appropriate executables:
client = GraphQL::Stitching::Client.from_definition(
File.read("schemas/supergraph.graphql"),
executables: {
products: GraphQL::Stitching::HttpExecutable.new(url: "https://products.myapp.com/graphql"),
users: GraphQL::Stitching::HttpExecutable.new(url: "http://users.myapp.com/graphql"),
my_local: MyLocalSchema,
}
)Some subgraph schemas may have overlapping root fields, such as the product field below. You may specify a root_entrypoints composer option to map overlapping root fields to a preferred location:
infos_schema = %|
type Product {
id: ID!
title: String!
}
type Query {
product(id: ID!): Product @stitch(key: "id")
}
|
prices_schema = %|
type Product {
id: ID!
price: Float!
}
type Query {
product(id: ID!): Product @stitch(key: "id")
}
|
client = GraphQL::Stitching::Client.new(
composer_options: {
root_entrypoints: {
"Query.product" => "infos",
}
},
locations: {
infos: {
schema: GraphQL::Schema.from_definition(infos_schema),
executable: #... ,
},
prices: {
schema: GraphQL::Schema.from_definition(prices_schema),
executable: #... ,
},
}
)In the above, selecting the root product field will route to the "infos" schema by default. You should bias root fields to their most general-purpose location. This option only applies to root fields where the query planner has no starting location bias (learn more about query planning). Note that type resolver queries are unaffected by entrypoint bias; a type resolver will always be accessed directly for a location when needed.
The strategy used to merge subgraph schemas into the combined supergraph schema is based on each element type:
-
Arguments of fields, directives, and
InputObjecttypes intersect for each parent element across locations (an element's arguments must appear in all locations):- Arguments must share a value type, and the strictest nullability across locations is used.
- Composition fails if argument intersection would eliminate a non-null argument.
-
ObjectandInterfacetypes merge their fields and directives together:- Common fields across locations must share a value type, and the weakest nullability is used.
- Objects with unique fields across locations must implement
@stitchaccessors. - Shared object types without
@stitchaccessors must contain identical fields. - Merged interfaces must remain compatible with all underlying implementations.
-
Enumtypes merge their values based on how the enum is used:- Enums used anywhere as an argument will intersect their values (common values across all locations).
- Enums used exclusively in read contexts will provide a union of values (all values across all locations).
-
Uniontypes merge all possible types from across all locations. -
Scalartypes are added for all scalar names across all locations. -
Directivedefinitions are added for all distinct names across locations:@visibilitydirectives intersect their profiles, see documentation.@stitchdirectives (both definitions and assignments) are omitted.