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

Inheritance support for input object types #515

Closed
MatthewChang opened this issue Jan 31, 2017 · 9 comments
Closed

Inheritance support for input object types #515

MatthewChang opened this issue Jan 31, 2017 · 9 comments

Comments

@MatthewChang
Copy link

I am currently facing a situation in which I have a create and update mutations for a certain type which have slightly different allowed arguments. Is there currently a built in way to do input type extension? I.e.

TodoUpdateInput = GraphQL::InputObjectType.define do
  name "TodoUpdateInput"
  argument :content, types.String
  argument :due_by, types.String
end

TodoCreateInput = GraphQL::InputObjectType.define do
  name "TodoCreateInput"
  extends [TodoUpdateInput]
  argument :author, !types.String
end

It's certainly possible to achieve more or less the same functionality by hacking around a bit but it might be nice to have something like this in the framework. Thanks!

@stephencelis
Copy link

The confusing bit is that certain things (like name) need to be unique and types in GraphQL aren't really "inheritable". Could you use vanilla mix-ins?

module TodoInputBase
  def self.included(input)
    argument :content, types.String
    argument :due_by, types.String
  end
end

TodoUpdateInput = GraphQL::InputObjectType.define do
  name "TodoUpdateInput"
  include TodoInputBase
end

TodoCreateInput = GraphQL::InputObjectType.define do
  name "TodoCreateInput"
  include TodoInputBase
  argument :author, !types.String
end

If it's a common pattern and you really wanna save some lines, you could write a wrapper:

def Included(&block)
  Module.new do
    define_singleton_method :included do |mod|
      mod.instance_eval(&block)
    end
  end
end

TodoInputBase = Included do
  argument :content, types.String
  argument :due_by, types.String
end

Vanilla functions are another option:

module TodoInputBase
  def self.apply(input)
    input.instance_eval do
      argument :content, types.String
      argument :due_by, types.String
    end
  end
end

TodoUpdateInput = GraphQL::InputObjectType.define do
  name "TodoUpdateInput"
  TodoInputBase.apply(self)
end

TodoCreateInput = GraphQL::InputObjectType.define do
  name "TodoCreateInput"
  TodoInputBase.apply(self)
  argument :author, !types.String
end

(You could similarly create a builder pattern here and shed a few lines.)

@MatthewChang
Copy link
Author

The syntax I settled on was

GraphQL::InputObjectType.accepts_definitions(
  argument_mixin: ->(defn, type) do
    type.arguments.each { |key, val| defn.arguments[key] = val.clone }
  end
)

TodoUpdateInput = GraphQL::InputObjectType.define do
  name "TodoUpdateInput"
  argument :content, types.String
  argument :due_by, types.String
end

TodoCreateInput = GraphQL::InputObjectType.define do
  name "TodoCreateInput"
  argument_mixin TodoUpdateInput
  argument :author, !types.String
end

to mirror the interface pattern. Perhaps "argument_mixin" isn't the best name but it is more accurate and fits better with the naming convention of the other definable attributes. If nobody thinks that this kind of behavior should be added to the framework we can go ahead and close the issue.

@khamusa
Copy link
Contributor

khamusa commented Mar 22, 2017

To me @MatthewChang's solution seems nice.

Since we're just cloning, I would appreciate being able to specify the fields/args to be cloned:

clone_arguments UserInputType, only: ['name', 'email']

Another option would be nullable:

clone_fields UserType, nullable: true

This would clone every field of UserType but voiding outer non-null constraints.
The use case is something like this:

Suppose in a Rails app, an UserType maps 1-to-1 with an User model. When I try creating a new user through a given mutation, there may be validation errors. If a validation error occur, the User instance returned is not considered valid and hence may have many missing fields (such as an id). Now, I'd like to be able to return an UserType as result for the mutations, but I can't since UserType is plenty of non-nullable fields, which may or may not be present in an invalid User. So I have to create an almost identical clone of UserType, except that id and a few other fields must be nullable.

What do you think? Is this too specific to be generically supported by the library?

@rmosolgo
Copy link
Owner

👍 I think it's great, I might just look for a more declarative-sounding name like include_arguments, but I'm not sure

@khamusa
Copy link
Contributor

khamusa commented Mar 27, 2017

We ended up with the following in our project:

GraphQL::InputObjectType.accepts_definitions(
  # Usage examples:
  #
  # 1. clones every argument from DogInputType
  #  inherit_arguments DogInputType
  #
  # 2. Clones every argument, except a few (single value or array of values accepted)
  #  inherit_arguments DogInputType, except: :name
  #
  # 3. Clones only the whitelisted arguments
  #  inherit_arguments DogInputType, only: [:name, :height]
  #
  # 4. The nullable option will remove eventual non-null constraints:
  #  inherit_arguments DogInputType, only: [:name, :height], nullable: true
  #
  # Note: nullable: true will only remove the external non-null checks:
  #   [String!]! will become [String!]
  inherit_arguments: ->(defn, from_type, opts = {}) do
    to_inherit = from_type.arguments.keys

    to_inherit -= Array(opts[:except]).map(&:to_s) if opts.key?(:except)
    to_inherit &= Array(opts[:only]).map(&:to_s)   if opts.key?(:only)

    to_inherit.each do |key|
      new_field = from_type.arguments[key.to_s].clone

      if opts[:nullable] && new_field.type.kind == GraphQL::TypeKinds::NON_NULL
        new_field.type = new_field.type.of_type
      end

      defn.arguments[key] = new_field
    end
  end
)

@abepetrillo
Copy link

abepetrillo commented May 17, 2017

Just tried adding this myself. Doesn't seem to be working for me @khamusa. Is there a particular place I should be putting this code? Going to try requiring it explicitly before the type using the method, will see how that goes (UPDATE: didn't work).

Error I'm experiencing is: GraphQL::Relay::Mutation can't define 'inherit_arguments'

Update:

To solve my problem I had to call this directly on GraphQL::Relay::Mutation. i.e.

GraphQL::Relay::Mutation.accepts_definitions(
...
)

@khamusa
Copy link
Contributor

khamusa commented May 17, 2017

@abepetrillo I'm glad you managed to make it work. We've used the above solution successfully. However we also realized that in our case using only / except options would lead to a quite bug-prone, brittle code. Instead, it seems to works better when we inherit all fields of a given type, thinking of types as we think of classes that do not violate the Liskov substitution principle. So, the idea is: think of base types, and inherit all their properties instead of a subselection.

@rmosolgo
Copy link
Owner

rmosolgo commented Nov 2, 2017

this will be available after #1037, please keep an eye on that issue!

@rmosolgo rmosolgo closed this as completed Nov 2, 2017
@tuval10
Copy link

tuval10 commented May 4, 2021

I don't see any documentation on how to do in the new class-based syntax
I've tried with interfaces - but they don't have #argument only #field

a way of how I did it in the new class-based syntax:

module Types
  class ChildInput < Types::BaseInputObject
    description 'Child input with name argument'

    def self.define_arguments(klass = self)
      # define all your arguments here like this:
      klass.argument :name, String, required: false
    end

    define_arguments

    # defines arguments on inheriting classes
    def self.inherited(base)
      define_arguments(base)
    end
  end

  class ParentInput < ChildInput
    argument :child_count, Int, required: false
  end
end

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

6 participants