Apollo GraphQL Custom Scalars
A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend
This is a 1st of the series blogs on deep dive into Apollo GraphQL from backend to frontend. A lot of information is Apollo GraphQL Doc or GraphQL Doc as well as their source code on Github — all tributes go to them. For my part, I would like to give you my “destructuring” of the original knowledge and my reflection on it, analysis on the code examples/source code as well as some extra examples.
For people who start to learn about GraphQL, they know that every type definition in a GraphQL schema belongs to one of the following categories:
- Scalar types
- Object types
- The
Query
type - The
Mutation
type - Input types
- Enum types
And GraphQL’s default scalar types:
Int
: A signed 32‐bit integerFloat
: A signed double-precision floating-point valueString
: A UTF‐8 character sequenceBoolean
:true
orfalse
ID
(serialized as aString
): A unique identifier that's often used to refetch an object or as the key for a cache. Think of it as Primary Key for a normal relational database.
For someone who is experienced in a strongly typed language like TypeScript, this is not difficult to understand.
Custom Scalar
From time to time, you will have the need to create your own scalar. While there is an amazing npm package for that: Graphql-scalars, which should satisfy most of your needs, but still, you may need to create something of your own.
You define these interactions in an instance of the GraphQLScalarType
class.
class GraphQLScalarType<InternalType> {
constructor(config: GraphQLScalarTypeConfig<InternalType>)
}type GraphQLScalarTypeConfig<InternalType> = {
name: string;
description?: ?string;
serialize: (value: mixed) => ?InternalType;
parseValue?: (value: mixed) => ?InternalType;
parseLiteral?: (valueAST: Value) => ?InternalType;
}
Let’s see an end to end implementation example on the GraphQL JS (Also on Apollo Server Doc). Note that after we define the scalar type we need to include it in the resolver map.
const { ApolloServer, gql } = require('apollo-server');
const { GraphQLScalarType, Kind } = require('graphql');const typeDefs = gql`
scalar Odd # define ittype MyType {
oddValue: Odd
}
`;const resolvers = {
Odd: new GraphQLScalarType({
name: 'Odd',
serialize: oddValue,
parseValue: oddValue,
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return oddValue(parseInt(ast.value, 10));
}
return null;
}
});
}function oddValue(value) {
return value % 2 === 1 ? value : null;
}const server = new ApolloServer({ typeDefs, resolvers });server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Another example with Date
scalar more on the details:
const { GraphQLScalarType, Kind } = require('graphql');const dateScalar = new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
serialize(value) {
return value.getTime();
},
parseValue(value) {
return new Date(value);
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return new Date(parseInt(ast.value, 10));
}
return null; // Invalid hard-coded value (not an integer)
},
});
You can see there are three important methods that describe how Apollo Server interacts with the scalar.
- Serialize: Convert ougoing scalar type to JSON when send it back to a client.
The value as argument is the Date
scalar in a GraphQL response, and we serialize it as the integer value returned by the getTime
function of a JavaScript Date
object.
- ParseValue: Parse client input that was passed through variables.
The value as argument comes from the variables
field in the incoming query as a JSON value and we need to convert to its back-end representation of Date type before it's added to a resolver's args
. This happens when the scalar is provided by a client as a GraphQL variable for an argument.
- ParseLiteral: Parse client input that was passed inline in the query.
When an incoming query string includes the scalar as a hard-coded argument value, that value is part of the query document’s abstract syntax tree (AST). In the example above, parseLiteral
converts the AST value from a string to an integer, and from integer to Date
.
Also note the differences on parseValue and parseLiteral is that parseValue gets a plain JS object, and parseLiteral gets a value AST. The differences are because in GraphQL there are two ways of reading input from client, one is inline in the query where 10
is the inline value for first
argument.
query {
allUsers(first:10) {
id
}
}
The other way of reading input from clients is through variables:
query ($first: Int) {
allUsers(first: $first) {
id
}
}variables: {
"frist": 10
}
Note that the Kind
from ParseLiteral
above is an enum that describes the different kinds of AST nodes. The helper Kind
contains all graphql types in more human-readable format and we can use it to determine the type of AST we want to check, for example with boolean:
import { Kind } from 'graphql';
function parseLiteral(value) {
return ast.kind === Kind.BOOLEAN ? ast.value : undefined;
}
Lastly, note that the DataScalar and the OddScalar above are using the GraphQLScalarType class directly, but you overwrite the class as the example shows below for MaxLength
.
import { GraphQLScalarType, GraphQLScalarTypeConfig } from "graphql";
export class MaxLength extends GraphQLScalarType {
constructor(
type: Readonly<GraphQLScalarTypeConfig<any, any>>,
limit: number
) {
super({
name: `MaxLength_${limit}`,
description: "Scalar type on max length",
serialize(value) {
const serialized = type.serialize(value);
if (typeof value === `string` && value.length <= limit) {
return serialized;
}
if (typeof value === `number` && !isNaN(value) && value <= limit) {
return serialized;
}
throw new TypeError(`Value above limit: ${limit}`);
},
parseValue(value) {
return type.parseValue(value);
},
parseLiteral(ast) {
return type.parseLiteral(ast, {});
},
});
}
}
We can use the Scalar above with custom Directive which we will talk about in the next blog.
That’s it!
Happy Reading!