Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GraphQL custom scalar definition without `graphql-tools`

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.

like image 404
Casey Avatar asked Dec 15 '17 01:12

Casey


1 Answers

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')
  }
})
like image 71
vbranden Avatar answered Oct 26 '22 08:10

vbranden