Introduction
In the GraphQL specification, we have different built-in scalar types. We have described them in the dedicated article. But what can we do if we need to use scalars which are not defined in the GraphQL specification? The answer is to use the so-called custom scalar. The implementation of the GraphQL custom scalar can vary based on the language you use for building GraphQL server. In some of these implementations, it may not be even possible to use custom scalars. In our case, we will on building custom scalar with our official graphql-js library.
We will continue with the same repository as with the enums and built-in scalars. You can clone the GitHub repository using this command
You can quickly start with:
git clone git@github.com:atherosai/graphql-gateway-apollo-express.git
install dependencies with
npm i
and start the server in development with
npm run dev
Custom scalar definition
As mentioned the built-in scalars might not be able to fulfil all the required parsing and validating of the input or output. That is the reason why we need to sometimes implement our own scalar. In this quick tutorial, we will implement DateTime custom scalar, which can be used for all date fields in your project. You can replicate all the steps also to build your scalars based on your needs. Now let’s go right into the designing of the DateTime scalar. We will use validator-js library to test if the value is in ISO8601 date-time string format. The simplified definition of the DateTime scalar can be as follows:
import { GraphQLScalarType } from 'graphql';import { isISO8601 } from 'validator';// This is only very simple DateTime scalar to show how to create your custom scalars. You can use some of the libraries for production use cases.const parseISO8601 = (value: any) => {if (isISO8601(value)) {return value;}throw new Error('DateTime cannot represent an invalid ISO-8601 Date string');};const serializeISO8601 = (value: any) => {if (isISO8601(value)) {return value;}throw new Error('DateTime cannot represent an invalid ISO-8601 Date string');};const parseLiteralISO8601 = (ast: any) => {if (isISO8601(ast.value)) {return ast.value;}throw new Error('DateTime cannot represent an invalid ISO-8601 Date string');};const DateTime = new GraphQLScalarType({name: 'DateTime',description: 'An ISO-8601 encoded UTC date string.',serialize: serializeISO8601,parseValue: parseISO8601,parseLiteral: parseLiteralISO8601,});export default DateTime;
In graphql-js, each custom scalar type definition has different fields and methods that should be defined. Just as with input/output object types, we have to define the required field name of the scalar. The description field is once again mandatory. If you read the article on built-in scalars we went through input and result coercion for each type. This is already predefined in graphql-js library for built-in scalars. However, when we define our own custom scalar, we have to specify these rules. That is why we have to define a couple of methods for each custom scalar. These are:
- serialize: result coercion
- parseValue: input coercion for variables
- parseLiteral: input coercion for inline arguments
Serialize
The serialize function refers to the result coercion. The first argument for this function is the received value itself. In our case, we would like to check if the value is in ISO8601 date format. If the received value passes the validation, we want to return this value; otherwise, we would raise the GraphQL error.
ParseValue and parseLiteral
The parseValue and parseLiteral function refer to the input coercion. The difference is that in parseValue, we refer to the input passed using the variables. When we use parseValue, the first argument of this function is the value itself. On the other hand, the parseLiteral function has its first argument the ast value in the following format
{ "kind": "StringValue","value": "2017-10-06T14:54:54+00:00","loc": { "start": 51, "end": 78 } }
That is why we have to extract the value from the ast variable and again validate it by our rules.
Implementation
We have completed the definition of our custom scalar, and therefore it is possible to implement it in the schema. We will apply it on the timestamp fields createdAt and updatedAt for our Task type. We can simply import our Date Time scalar and use it as a type for these fields.
import {GraphQLString,GraphQLID,GraphQLObjectType,GraphQLNonNull,GraphQLInt,GraphQLFloat,GraphQLBoolean,} from 'graphql';import DateTime from '../custom-scalars/DateTime';import TaskStateEnumType from './TaskStateEnumType';const TaskType = new GraphQLObjectType({name: 'Task',fields: () => ({id: {type: new GraphQLNonNull(GraphQLID),},name: {type: new GraphQLNonNull(GraphQLString),},completed: {type: new GraphQLNonNull(GraphQLBoolean),defaultValue: false},state: {type: new GraphQLNonNull(TaskStateEnumType),},progress: {type: new GraphQLNonNull(GraphQLFloat),},taskPriority: {type: new GraphQLNonNull(GraphQLInt),},dueDate: {type: DateTime,},createdAt: {type: new GraphQLNonNull(DateTime),},updatedAt: {type: DateTime,},}),});export default TaskType
Now let’s check out if our rules, which we defined works as expected. Just go to GraphQL Playground and try to call the basic getTasks query:
query getTasks {tasks {idnametaskPriorityprogresscreatedAtupdatedAt}}
If our custom Date Time scalar is implemented correctly, you should get something like this:
{"data": {"tasks": [{"id": "7e68efd1","name": "Test task","taskPriority": 1,"progress": 55.5,"createdAt": "2017-10-06T14:54:54+00:00","updatedAt": "2017-10-06T14:54:54+00:00"}]}}
Now let’s test out our serialize function. Just go to the task-db.ts file and reassign createdAt or updatedAt to a string which is not in ISO8601 date format. We will change the createdAt field to 2017–10–06T14:54:54+0. Now if we try to call the getTasks Query, the GraphQL server will raise the following error:
{"data": {"tasks": [null]},"errors": [{"message": "DateTime cannot represent an invalid ISO-8601 Date instance","locations": [{"line": 7,"column": 5}],"path": ["tasks",0,"createdAt"]}]}
Summary
Custom scalars provide an even greater benefit when we have a collection of our predefined custom scalars. Then we can centralize most of our custom validations and move them from our resolver functions to our custom scalar types. Our simple Date Time scalar was a great model example. However, if you want to use more complex scalars like JSON or more precisely defined Date Time scalars, etc., it is possible to use some of the open-sourced npm packages. These include:
- JSON: Using JSON as a scalar.
- Date Time: It is a library you can use for more precisely defined Date Time scalar. In this library, Time and Date scalars are also available.
- Custom scalar collection: This package is a collection of custom scalars .
Did you like this post? The repository with the examples and project set-up can be cloned here. Feel free to send any questions about the topic to david@atheros.ai.