Custom Scalars
A custom scalar is a scalar type in your schema where the underlying value is (somewhat) opaque to the client.
Custom scalars are defined using the @gql.scalar
attribute in ResGraph. Here follows a description of how you can leverage simple and more advanced type setups for custom scalars.
Simple custom scalars
The simplest way to define a custom scalar is to use a type alias:
/** A timestamp. */
@gql.scalar
type timestamp = float
"""
A timestamp.
"""
scalar Timestamp
A simple custom scalar has the following restrictions:
- Must be a type alias.
- The type alias must be to a type that's a valid GraphQL type.
Caveats
Because this is a type alias, you'll need to use explicit type annotations to tell ResGraph that you're looking to use your new scalar type rather than the underlying primitive.
@gql.scalar
type timestamp = float
@gql.field
let currentTime = (_: query) => {
Date.now()
}
scalar Timestamp
type Query {
currentTime: Float!
}
Notice that there's no way for ResGraph to know that you meant to use your Timestamp
scalar, and not float
. You solve this either by annotating the function return type with timestamp
, or by assinging the value you want to return to a value that's annotated with timestamp
, and then return that value. Both examplified here:
@gql.scalar
type timestamp = float
@gql.field
let currentTime = (_: query): timestamp => {
// Either annotate the return type, like above,
// or annotate an intermediate value, like below:
let time: timestamp = Date.now()
time
}
Now ResGraph understands that you're returning a custom scalar:
scalar Timestamp
type Query {
currentTime: Timestamp!
}
Opaque types and custom scalars
The ease of leveraging opaque types is one of the main strengths of ReScript. ResGraph makes it easy to leverage them when building your GraphQL server as well.
To define a custom scalar backed by an abstract type, do this:
module Timestamp: {
/** A timestamp. */
@gql.scalar
type t
let make: unit => t
} = {
type t = float
let make = () => Date.now()
}
@gql.field
let currentTime = (_: query) => {
Timestamp.make()
}
"""
A timestamp.
"""
scalar Timestamp
type Query {
currentTime: Timestamp!
}
Notice a few things:
- The opaque type must be named exactly
t
, and annotated with@gql.scalar
. - The name of the custom scalar will be derived from the module name when the type is
t
.
Notice also that while the type t
is opaque in your server, you're not forced to show how to serialize and parse your custom scalar if its underlying type is a valid GraphQL type. This is because ResGraph will look at your implementation.
However, there are cases when you either must define a way to parse and serialize a custom scalar, because its underlying represenation isn't a valid GraphQL type. Or when you want to control how the custom scalar is parsed and serialized for other reasons.
In those cases, you can define a custom scalar with a custom parser and serializer:
Custom scalars that need custom parsing and serializing
You define a custom scalar that needs a custom parser and serializer like this:
module Datetime: {
/** A date. */
@gql.scalar
type t
let parseValue: ResGraph.GraphQLLiteralValue.t => option<t>
let serialize: t => ResGraph.GraphQLLiteralValue.t
} = {
type t = Date.t
open ResGraph.GraphQLLiteralValue
let parseValue = v =>
switch v {
| String(str) => Some(Date.fromString(str))
| Number(timestamp) => Some(Date.fromTime(timestamp))
| _ => None
}
let serialize = d => d->Date.toJSON->Option.getExn->String
}
"""
A date.
"""
scalar Datetime
Let's distill what's going on here:
Datetime.t
is opaque, and the underlying type isDate.t
, which isn't a valid GraphQL type.- We define
parseValue: ResGraph.GraphQLLiteralValue.t => option<t>
andserialize: t => ResGraph.GraphQLLiteralValue.t
. These need to be defined exactly like this, as in be called those names, and useGraphQLLiteralValue.t
+ the localt
type. parseValue
is responsible for parsing the value GraphQL gives you at runtime, into your localt
.serialize
is responsible for turning yourt
into a literal value that can be transferred to the client.
With this, your custom scalar can now be serialized and parsed even if it isn't backed by a valid GraphQL type.