Unions, Interfaces and Scalars
The SDL declares every type. Object types resolve their fields through directives, the default reader, or a type class. Three more kinds need Ruby to say how their values work:
- a union or an interface needs to know which object type each value is;
- a custom scalar needs to know how to read its values from a query and write them to a response.
Lighthouse answers both as PHP Lighthouse does by default, and lets you take over with a class, in graphql-ruby's app/graphql/types structure, or with a directive.
Unions and interfaces
type Query {
search(term: String!): [SearchResult!]!
latestActivity: [Authored!]!
}
union SearchResult = Post | User
interface Authored {
id: ID!
author: User!
}
type Post implements Authored { id: ID! title: String! author: User! }
type Comment implements Authored { id: ID! body: String! author: User! }
A resolver returns records, of any of the types:
class Resolvers::Search < Resolvers::BaseResolver
def resolve(term:)
Post.where('title LIKE ?', "%#{term}%").to_a + User.where('name LIKE ?', "%#{term}%").to_a
end
end
{
search(term: "Grace") {
__typename
... on Post { title }
... on User { name }
}
}
The default
Each value resolves to the first of these that is one of the union's (or the interface's) types:
- a Hash's
__typename({ __typename: 'Movie', title: 'Heat' }); - the type named after the value's class (a
Postrecord is aPost), or after one of its superclasses, so anAdmin < UserSTI record is aUserwhen noAdmintype exists; - the type whose
@modelnames the value's model. Withtype Article @model(class: "Post"), aPostrecord is anArticle.
Most schemas need nothing else.
A type class
When the default is not enough, say when the values are Hashes from a search service rather than records, write the union's class as graphql-ruby does (rails g graphql:union SearchResult): Types::<Name>Type, with resolve_type. Its base, Types::BaseUnion, includes Lighthouse::GraphQL::Type after lighthouse:install (see opting in). It returns a type name, or a type class, which stands for the SDL type of its graphql_name:
# app/graphql/types/search_result_type.rb
module Types
class SearchResultType < BaseUnion
def self.resolve_type(object, context)
return unless object.is_a?(Hash)
object['index'] == 'users' ? Types::UserType : 'Post'
end
end
end
An interface's goes in graphql-ruby's definition_methods:
# app/graphql/types/authored_type.rb
module Types
module AuthoredType
include BaseInterface
definition_methods do
def resolve_type(object, context)
object['index'] == 'comments' ? 'Comment' : 'Post' if object.is_a?(Hash)
end
end
end
end
nil leaves the value to the default, so the class only handles the cases that need it. The SDL still declares the members: possible_types in the class are ignored.
@union and @interface
PHP Lighthouse's directives name the class in the schema instead. It has resolve_type(object, context) or call(object, context), and is looked up in the types namespace (nested names too), then as written:
union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
The directive comes first, then the type class, then the default.
When nothing resolves a value
A value no step resolves, or one resolved to a type the union or interface does not include, raises Lighthouse::DefinitionError. It is a server bug, not a client error, and the message names the ways out:
Lighthouse could not tell the type of a Tag in the union SearchResult (Post | User):
name a type after its class, map one to it with @model(class: "Tag"), or resolve
it in Types::SearchResultType.resolve_type or with @union(resolveType:).
A @union or @interface naming no class, or a type class of another kind than its SDL type (a union class for an interface), fails when the schema is built.
Scalars
"An ISO 8601 date and time."
scalar DateTime
type Post {
createdAt: DateTime!
}
type Query {
latestActivity(since: DateTime): [Authored!]!
}
Write the scalar's class with graphql-ruby's rails g graphql:scalar DateTime: Types::<Name>Type, a Types::BaseScalar, which includes Lighthouse::GraphQL::Type after lighthouse:install:
# app/graphql/types/date_time_type.rb
module Types
class DateTimeType < BaseScalar
def self.coerce_input(value, context)
Time.iso8601(value.to_s)
rescue ArgumentError
raise GraphQL::CoercionError, "#{value.inspect} is not an ISO 8601 date and time"
end
def self.coerce_result(value, context)
value.utc.iso8601
end
end
end
coerce_inputreads an argument or variable. The resolver gets its result, here aTime. RaisingGraphQL::CoercionError(or returningnil) makes the argument invalid, and the client gets the message.coerce_resultwrites a value a field returns, herePost#created_at.
@scalar(class:), PHP Lighthouse's directive, names the class in the schema instead, looked up in the types namespace, then as written:
scalar Money @scalar(class: "Money::GraphQLScalar")
A scalar with neither passes its values through unchanged. A @scalar naming no class with coerce_input and coerce_result fails when the schema is built.
Opting in, and base classes
Only a class including Lighthouse::GraphQL::Type counts: any other Types::SearchResultType or Types::DateTimeType is left alone. lighthouse:install includes the concern in graphql-ruby's Types::BaseObject, BaseInterface, BaseUnion and BaseScalar, so every type class inheriting from them counts. In an app that did not run it, include the concern by hand, in those bases or in each class: a subclass of GraphQL::Schema::Object, Union or Scalar, or a module including GraphQL::Schema::Interface.
The classes are looked up in namespaces[:types] (default ['Types']). When the schema's base_types name a base for a kind, graphql-ruby builds the SDL's types of that kind from it, and a type class of that kind must inherit from it, or the schema does not build:
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
options: {
base_types: {
object: Types::BaseObject,
union: Types::BaseUnion,
interface: Types::BaseInterface,
scalar: Types::BaseScalar
# query_type:, node:, resolver:, mutation:, ... (resolvers.md#handing-the-bases-to-the-schema)
}
}
)
end