GraphQL custom scalar definition without `graphql-tools`
Asked Answered
D

2

5

After reading this walkthrough in the official documentation:

http://graphql.org/graphql-js/object-types/

I am very confused about how to make custom scalar type resolvers without a third party library. Here is the sample code in the docs:

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

// Construct a schema, using GraphQL schema language
var schema = buildSchema(`
  type RandomDie {
    numSides: Int!
    rollOnce: Int!
    roll(numRolls: Int!): [Int]
  }

  type Query {
    getDie(numSides: Int): RandomDie
  }
`);

// This class implements the RandomDie GraphQL type
class RandomDie {
  constructor(numSides) {
    this.numSides = numSides;
  }

  rollOnce() {
    return 1 + Math.floor(Math.random() * this.numSides);
  }

  roll({numRolls}) {
    var output = [];
    for (var i = 0; i < numRolls; i++) {
      output.push(this.rollOnce());
    }
    return output;
  }
}

// The root provides the top-level API endpoints
var root = {
  getDie: function ({numSides}) {
    return new RandomDie(numSides || 6);
  }
}

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');

I understand I can use graphql-tools to make "executable schema" from string-based type definitions and a resolvers object. What I'm wondering is why there is no lower level / imperative graphql-js API I can use to define and resolve custom scalar types? In other words, how does graphql-tools even work?

Thanks in advance!


Edit:

Here is some example code outlining the problem. On line 4 you can see that I am importing GraphQLJSON but it is never used. I know what to do to make this work using graphql-tools but I want to learn how it works. In other words, if graphql-tools did not exist, what would I do to inject a custom scalar type while still authoring my schema using graphql syntax? From what I can tell the only graphql-js solution is to use the non-declarative approach to authoring schema (second example below)

import express from 'express';
import graphqlHTTP from 'express-graphql';
import { buildSchema } from 'graphql';
import GraphQLJSON from 'graphql-type-json'; // where should I inject this?

const schema = buildSchema(`
  type Image {
    id: ID!
    width: Int!
    height: Int!
    metadata: JSON!
  }

  type Query {
    getImage(id: ID!): Image!
  }

  scalar JSON
`);

class Image {
  constructor(id) {
    this.id = id;
    this.width = 640;
    this.height = 480;
  }
  metadata() {
    // what do I need to do in order to have this return value parsed by GraphQLJSON
    return { foo: 'bar' };
  }
}

const rootValue = {
  getImage: function({ id }) {
    return new Image(id);
  },
};

const app = express();
app.use(
  '/graphql',
  graphqlHTTP({
    schema: schema,
    rootValue: rootValue,
    graphiql: true,
  })
);
app.listen(4000);

Running this query:

{
    getImage(id: "foo") {
    id
    width
    height
    metadata
  }
}

Results in this error:

Expected a value of type \"JSON\" but received: [object Object]

The answer I'm seeking would help me to return the JSON type without using graphql-tools. I have nothing against this library, but it seems bizarre to me that I must use a third party library for something so fundamental to the type resolution system in graphql-js. I would like to know more about why this dependency is needed before adopting it.

Here is another way to make this work:

import { GraphQLObjectType, GraphQLInt, GraphQLID } from 'graphql/type';

const foo = new GraphQLObjectType({
  name: 'Image',
  fields: {
    id: { type: GraphQLID },
    metadata: { type: GraphQLJSON },
    width: { type: GraphQLInt },
    height: { type: GraphQLInt },
  },
});

However this does not allow me to author my schema using the graphql syntax, which is my goal.

Diffractometer answered 15/12, 2017 at 1:34 Comment(0)
L
16

UPDATE

After some clarification, it looks like you are trying to add an a custom scalar to a schema created with schema language. Since schemas built buildSchema (or other client tools) do not have handler functions for serialize, parseValue, and parseLiteral bound, you need to modify the built schema to include those. you can do something like

import { buildSchema } from 'graphql'
import GraphQLJSON from 'graphql-type-json'

const definition = `
type Foo {
  config: JSON
}

scalar JSON

Query {
  readFoo: Foo
}

schema {
  query: Query
}`

const schema = buildSchema(definition)
Object.assign(schema._typeMap.JSON, GraphQLJSON)

Alternately you can also do the following which may be useful for renaming the scalar to something else

Object.assign(schema._typeMap.JSON, {
  name: 'JSON',
  serialize: GraphQLJSON.serialize,
  parseValue: GraphQLJSON.parseValue,
  parseLiteral: GraphQLJSON.parseLiteral
})

Original Answer

buildSchema indeed creates a schema but that schema will have no resolve, serialize, parseLiteral, etc. functions associated with it. I believe graphql-tools only allows you to map resolver functions to fields which does not help you when you are trying to create a custom scalar.

graphql-js has a GraphQLScalarType you can use to build custom scalars. see official documentation and example at http://graphql.org/graphql-js/type/#graphqlscalartype

There are also several packages in npm that you can use as an example

one i find very useful is https://github.com/taion/graphql-type-json/blob/master/src/index.js

as an example if you wanted to create a base64 type that stores a string as base64 and decodes base64 strings before they are returned in the response you can create a custom base64 scalar like this

import { GraphQLScalarType, GraphQLError, Kind } from 'graphql'

const Base64Type = new GraphQLScalarType({
  name: 'Base64',
  description: 'Serializes and Deserializes Base64 strings',
  serialize (value) {
    return (new Buffer(value, 'base64')).toString()
  },
  parseValue (value) {
    return (new Buffer(value)).toString('base64')
  },
  parseLiteral (ast) {
    if (ast.kind !== Kind.STRING) {
      throw new GraphQLError('Expected Base64 to be a string but got: ' + ast.kind, [ast])
    }
    return (new Buffer(ast.value)).toString('base64')
  }
})
Lassitude answered 15/12, 2017 at 7:40 Comment(6)
Thanks for your response. I understand how to create custom scalar types -- this is well documented. I will edit my question to provide a more specific example outlining what's going wrongDiffractometer
if your question is around how do you set the serialize, parseValue, and parseLiteral handlers for a custom scalar using a string definition, you need to use graphql-js buildSchema to create the schema, or whatever tool creates the schema from it and then you can set schema._typeMap.CustomScalarName.serialize = serializeHandler, etcLassitude
I did try schema._typeMap.JSON = GraphQLJSON but this did not workDiffractometer
you cant do it that way. you need to add scalar JSON to the string definition then you can do something like Object.assign(schema._typeMap.JSON, GraphQLJSON)Lassitude
Object.assign works -- awesome! Can you please edit your answer and I will mark it as accepted for future viewers? THANK YOUDiffractometer
Great answer. To extend it, if somebody is using GraphQLScalarType to create their custom scalar they have to assign Foo._scalarConfig, being Foo the scalar. Example with Date: Object.assign(schema._typeMap.Date, Date._scalarConfig);Twi
C
0

Beautiful answer from @vbraden . This helped me as well to put date type in my graphQL using buildSchema just like OP. I'm posting to share that for anyone else who landed here trying to put dates in their schema, and to signal boost @vbraden's awesome help. Confirmed it works!

import { buildSchema } from "graphql";
import pkg from 'graphql-iso-date'
const { GraphQLDateTime } = pkg


const definition = `
    type Account {
        accountNumber: String,
        dateOpened: DateTime,
        dateClosed: DateTime,
        currentBalance: String,
    }

    scalar DateTime

    type Accounts {
        accounts: [Account!]!
    }

    type RootQuery {
        accounts: Accounts
    }

    schema {
        query: RootQuery
    }
`;

const schema = new buildSchema(definition);
Object.assign(schema._typeMap.DateTime, GraphQLDateTime)

export default schema;
Chronometry answered 18/8, 2022 at 20:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.