Can you make a graphql type both an input and output type?
Asked Answered
C

4

66

I have some object types that I'd like to use as both input and output - for instance a currency type or a reservation type.

How do I define my schema to have a type that supports both input and output - I don't want to duplicate code if I don't have to. I'd also prefer not to create duplicate input types of things like currency and status enums.

export const ReservationInputType = new InputObjectType({
  name: 'Reservation',
  fields: {
    hotelId: { type: IntType },
    rooms: { type: new List(RoomType) },
    totalCost: { type: new NonNull(CurrencyType) },
    status: { type: new NonNull(ReservationStatusType) },
  },
});

export const ReservationType = new ObjectType({
  name: 'Reservation',
  fields: {
    hotelId: { type: IntType },
    rooms: { type: new List(RoomType) },
    totalCost: { type: new NonNull(CurrencyType) },
    status: { type: new NonNull(ReservationStatusType) },
  },
});
Copro answered 6/1, 2017 at 22:54 Comment(0)
M
42

In the GraphQL spec, objects and input objects are distinct things. Quoting the spec for input objects:

Fields can define arguments that the client passes up with the query, to configure their behavior. These inputs can be Strings or Enums, but they sometimes need to be more complex than this.

The Object type... is inappropriate for re‐use here, because Objects can contain fields that express circular references or references to interfaces and unions, neither of which is appropriate for use as an input argument. For this reason, input objects have a separate type in the system.

An Input Object defines a set of input fields; the input fields are either scalars, enums, or other input objects. This allows arguments to accept arbitrarily complex structs.

While an implementation might provide convenience code to create an object and a corresponding input object from a single definition, under the covers, the spec indicates that they'll have to be separate things (with separate names, such as Reservation and ReservationInput).

Milled answered 6/1, 2017 at 23:14 Comment(6)
where is it stated that about the separate names for type and input?Connected
@4F2E4A2E: Well, the section of the specification that I quoted covers this (Object versus Input Object).Milled
@4F2E4A2E: "it is possible to have both definitions with the same name but with different types, right?" -- not to the best of my knowledge. Symbols have to be unique in most programming languages.Milled
Thanks, i've tested it out, it's clearly not possible. @MonkeyBonkey: don't forget to mark this as answered.Connected
That this is not possible is a major flaw from a developer perspective. I hope they had a good reason to set it up like this. Leads to very WET code.Haig
"All types within a GraphQL schema must have unique names. No two provided types may have the same name." GraphQL / SchemaNedrud
H
13

While working on a project I had a similar problem with code duplication between input and type objects. I did not find the extend keyword very helpful as it only extended the fields of that specific type. So the fields in type objects cannot not be inherited in input objects.

In the end I found this pattern using literal expressions helpful:

const UserType = `
    name: String!,
    surname: String!
`;

const schema = graphql.buildSchema(`
    type User {
        ${UserType}
    }
    input InputUser {
        ${UserType}
    }
`) 
Hoeg answered 8/8, 2020 at 12:6 Comment(3)
This is the best solution I think. Personally though I prefer to use a file.graphql because its distinctive and removes complication. I think the creators of graphql should be working on a solution to share the data between type and input via a variable type.Conflagration
@SteveTomlin do you know how to solve it in .graphql files? I can't find any referencePatel
Sure. An example: import * as schemaGame from './_game.graphql';Conflagration
G
6

You can do something like this:

export const createTypes = ({name, fields}) => {
  return {
    inputType: new InputObjectType({name: `${name}InputType`, fields}),
    objectType: new ObjectType({name: `${name}ObjectType`, fields})
  };
};

const reservation = createTypes({
  name: "Reservation",
  fields: () => ({
    hotelId: { type: IntType },
    rooms: { type: new List(RoomType) },
    totalCost: { type: new NonNull(CurrencyType) },
    status: { type: new NonNull(ReservationStatusType) }
  })
});
// now you can use:
//  reservation.inputType
//  reservation.objectType
Grossman answered 5/4, 2018 at 11:7 Comment(1)
I upvoted for the idea, but very hacky in my opinionHaig
T
1

this is something that i did for my project (works good):

const RelativeTemplate = name => {
  return {
    name: name,
    fields: () => ({
      name: { type: GraphQLString },
      reference: { type: GraphQLString }
    })
  };
};
const RelativeType = {
  input: new GraphQLInputObjectType(RelativeTemplate("RelativeInput")),
  output: new GraphQLObjectType(RelativeTemplate("RelativeOutput"))
};
Transmundane answered 23/8, 2018 at 9:53 Comment(1)
won't work if the shared fields are non scalar fields :(Cynewulf

© 2022 - 2024 — McMap. All rights reserved.