Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GraphQL how to avoid duplicate code between input and output types

I'am new to GraphQL but I really like it. Now that I'am playing with interfaces and unions, I'am facing a problem with mutations.

Suppose that I have this schema :

interface FoodType {
    id: String
    type: String
    composition: [Ingredient]
  }

  type Pizza implements FoodType {
    id: String
    type: String
    pizzaType: String
    toppings: [String]
    size: String
    composition: [Ingredient]
  }

  type Salad implements FoodType {
    id: String
    type: String
    vegetarian: Boolean
    dressing: Boolean
    composition: [Ingredient]
  }

  type BasicFood implements FoodType {
    id: String
    type: String
    composition: [Ingredient]
  }

  type Ingredient {
      name: String
      qty: Float
      units: String
  }

Now, I'd like to create new food items, so I started doing something like this :

type Mutation {
    addPizza(input:Pizza):FoodType
    addSalad(input:Salad):FoodType
    addBasic(input:BasicFood):FoodType
}

This did not work for 2 reasons :

  1. If I want to pass an object as parameter, this one must be an "input" type. But "Pizza", "Salad" and "BasicFood" are just "type".
  2. An input type cannot implement an interface.

So, I need to modify my previous schema like this :

interface FoodType {
    id: String
    type: String
    composition: [Ingredient]
}

type Pizza implements FoodType {
    id: String
    type: String
    pizzaType: String
    toppings: [String]
    size: String
    composition: [Ingredient]
}

type Salad implements FoodType {
    id: String
    type: String
    vegetarian: Boolean
    dressing: Boolean
    composition: [Ingredient]
}

type BasicFood implements FoodType {
    id: String
    type: String
    composition: [Ingredient]
}

type Ingredient {
        name: String
        qty: Float
        units: String
}

type Mutation {
    addPizza(input: PizzaInput): FoodType
    addSalad(input: SaladInput): FoodType
    addBasic(input: BasicInput): FoodType    
}

input PizzaInput {
    type: String
    pizzaType: String
    toppings: [String]
    size: String
    composition: [IngredientInput]
}

input SaladInput {
    type: String
    vegetarian: Boolean
    dressing: Boolean
    composition: [IngredientInput]
}

input BasicFoodInput {
    type: String
    composition: [IngredientInput]
}

input IngredientInput {
        name: String
        qty: Float
        units: String
}

So, here I defined my 3 creation methods for Pizza, Salad and Basic food. I need to define 3 input types (one for each food) And I also need to define a new input type for Ingredients.

It makes lot of duplication. Are you ok with that? Or there is a better way to deal with this?

Thank you

like image 801
Fred Mériot Avatar asked Jan 16 '18 09:01

Fred Mériot


1 Answers

There's a handful of things you could do. For example, if you were to declare your schema programatically, you can get away with something like this:

const getPizzaFields = (isInput = false) => {
  const fields = {
    type: { type: GraphQLString }
    pizzaType: { type: GraphQLString }
    toppings: { type: new GraphQLList(GraphQLString) }
    size: { type: GraphQLString }
    composition: {
      type: isInput ? new GraphQLList(IngredientInput) : new GraphQLList(Ingredient)
    }
  }
  if (!isInput) fields.id = { type: GraphQLString }
  return fields
}

const Pizza = new GraphQLObjectType({
  name: 'Pizza',
  fields: () => getFields()
})

const PizzaInput = new GraphQLObjectType({
  name: 'Pizza',
  fields: () => getFields(true)
})

Or if your objects/inputs follow a similar pattern, you could even write a function for generating inputs from types:

const transformObject = (type) => {
  const input = Object.assign({}, type)
  input.fields.composition.type = new GraphQLList(IngredientInput)
  delete input.fields.id
  return input
}

Alternatively, when defining your schema using makeExecutableSchema, you could do:

const commonPizzaFields = `
    type: String
    pizzaType: String
    toppings: [String]
    size: String
`

const schema = `
  type Pizza {
    id: String
    ${commonPizzaFields}
    composition: [Ingredient]
  }

  input PizzaInput {
    ${commonPizzaFields}
    composition: [IngredientInput]
  }
`

The problem with all of these approaches is that while they technically may make your code more DRY, they also reduce your schema's readability, which in my opinion makes it even more error-prone than the duplication itself.

It's also important to understand that while syntactically, a Type and an Input type may appear the same, functionally they are not. For example, a field on a type may have arguments:

type Pizza {
  toppings(filter: ToppingTypeEnum): [String]
}

Input Type fields do not have arguments, so you would not be able to utilize the same syntax for a toppings field in both a Pizza Type and its counterpart PizzaInput Input Type.

Personally, I would bite the bullet and just write out both the types and the inputs like you've already done. The only thing I would do differently is grouping them together (listing your type than your input), so that any differences between the two are easy to spot. But your mileage may very :)

like image 192
Daniel Rearden Avatar answered Sep 30 '22 03:09

Daniel Rearden