Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is proper way to convert GraphQLObjectType to GraphQLInputObjectType? #312

Closed
nodkz opened this issue Mar 13, 2016 · 6 comments
Closed

Comments

@nodkz
Copy link
Contributor

nodkz commented Mar 13, 2016

I have RefType and it's widely using in other types:

import {
  GraphQLObjectType,
  GraphQLString,
} from 'graphql';

const RefType = new GraphQLObjectType({
  name: 'Ref',
  fields: {
    i: { type: GraphQLString, description: 'ObjectId as string' },
    t: { type: GraphQLString, description: 'ORM-model class name' },
  },
});

export default RefType;

Today I want to pass this type like input field for mutation:

const NotesAddMutation = mutationWithClientMutationId({
  name: 'NotesAdd',
  inputFields: {
    ref: { type: RefType },
    text: { type: GraphQLString },
  },
  outputFields: {
...

And get error NotesAddInput.ref field type must be Input Type but got: Ref. A couple minutes of googling gets answer to use GraphQLInputObjectType.

So I have several questions:

  1. Does GQ have some object type for my RefType, which I can use for queries and mutations? My RefType is like scalar type, but have two values. Analogous type may be LatitudeLongitudeType as example.
  2. What is proper way convert GraphQLObjectType to GraphQLInputObjectType? Or I should define two types (one type for queries, second for mutations and may be third in future, when subscriptions come)?
  3. May be error message can be improved, because right now it does not help to determine what is wrong?

PS. If somebody tries to find understanding of a design purpose GraphQLObjectType and GraphQLInputObjectType, you may read https://medium.com/@HurricaneJames/graphql-mutations-fb3ad5ae73c4#.wi9iwkgot

@freiksenet
Copy link
Contributor

I'll answer 1 and 3 first.

InputObjectType can only have other input types as it's members. This is because ObjectType might have mutually-recursive dependencies with other types, which can't be resolved to a JSON structure. Thus you can't use Ref directly in InputObjects. Error is just telling you that - ObjectType is not a kind of type you can use as input and thus as part of input object.

@freiksenet
Copy link
Contributor

Now for question number 2. There are multiple approaches to how to handle this, in some APIs, you might design all your inputs to be custom and thus never convert from object types. Otherwise we've followed the following approach at Reindex:

  • Go through all the fields of the ObjectType (through type.getFields())
  • If field is a scalar, add it to InputObjectType as is
  • If field is a ObjectType
    • if it can be referenced by an id (in our case, we check if ObjectType has Relay's Node interface), then add reference to it (id) to InputObject
    • if it is "inline" type, recursively call the conversion function on it, get InputObject and add it
  • If field is GraphQLNonNull or GraphQLList, inspect the inner type as above, and add result wrapped in corresponding wrapper

One thing to note - non-nullity has a bit different meaning in GraphQLObject and GraphQLInputObject. For input object it means that the field is required, for objects that means that not returning a non-null value for that field is an error. You might, in some cases, have non-null in InputObject, but not in an Object, because null is often a valid return value in GraphQL, eg when permissions are invalid.

@freiksenet
Copy link
Contributor

Here is example code that implements the above algorithm:

import { mapValues } from 'lodash';
import {
  GraphQLNonNull,
  GraphQLScalarType,
  GraphQLInputObjectType,
  GraphQLEnumType,
} from 'graphql';

export default function createInputObject(
  type
) {
  return new GraphQLInputObjectType({
     name: type.name + 'Input', 
     fields: mapValues(type.getFields(), (field) =>
       convertInputObjectField(field)
     ),
   });
}

function convertInputObjectField(
  field,
) {
  let fieldType = field.type;
  const wrappers = [];

  while (fieldType.ofType) {
    wrappers.unshift(fieldType.constructor);
    fieldType = fieldType.ofType;
  }

  if (!(fieldType instanceof GraphQLInputObjectType ||
        fieldType instanceof GraphQLScalarType ||
        fieldType instanceof GraphQLEnumType)) {
    fieldType = fieldType.getInterfaces().includes(NodeInterface) ?
      ID :
      createInputObject(fieldType)
  }

  fieldType = wrappers.reduce((type, Wrapper) => {
    return new Wrapper(type);
  }, fieldType);

  return { type: fieldType };
}

@nodkz
Copy link
Contributor Author

nodkz commented Mar 14, 2016

@freiksenet
YAY, many thanks!!!
Definitely, +100 to karma for next brewing ;)

@johanatan
Copy link
Contributor

johanatan commented Nov 15, 2016

@freiksenet Can you explain where NodeInterface is defined? I scanned the entire GraphQL source (0.8.1) and can't find it.

Also, did you mean GraphQLID rather than ID? If not, where is ID defined?

@nodkz
Copy link
Contributor Author

nodkz commented Nov 16, 2016

@johanatan you may use this ready solution:

import { TypeComposer } from 'graphql-compose';

const yourConvertedInputType = TypeComposer.create(YourOutputType).getInputType();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants