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

Create a compound input type #1750

Closed
guyzmo opened this issue Aug 8, 2018 · 2 comments
Closed

Create a compound input type #1750

guyzmo opened this issue Aug 8, 2018 · 2 comments

Comments

@guyzmo
Copy link

guyzmo commented Aug 8, 2018

tl;dr: my real need is what is proposed here. But meanwhile, I can live with a type that has for properties:

  • Only one of its fields is filled. If no fields is filled → error, if two or more fields are filled → error
  • When used as a field, it returns the data of the one field it has that has been setup

let's dig into what I want, and how I solved it:

• Let's consider a business function do_stuff(args) that takes an hash described by the graphql schema.
• Within that function, I need a key to be of two different kinds: either a string id of an object I already got in my system, or a more complex thing (a hash with many values).

So basically, here is do_stuff(args)

def do_stuff(args)
  if args[:thing].class.name == "String"
    print("I can do something with that #{args[:thing]}")
  else
    print("That thing is more complex, let's do something complex with it")
  end
end

That function do_stuff() is being used at other places, like in the Rest API or through a controller's method, so I don't want to include logic specific to handling the GraphQL schema within it.

In GraphQL, to keep a neat strongly and statically typed schema, I chose to make a compound type, that's as follows:

class Types::ThatComplexThingType < Types::BaseInputObject
  # arguments
end

class Types::ThatCompoundThing < Types::BaseInputObject
  argument :thing_id, String, required: false
  argument :thing_data, Types::ThatComplexThingType, required: false
end

and that type is being used in the mutations:

class Types::MyMutationAttributes < Types::BaseInputObject
  # …
  argument :thing, Types::ThatCompoundThing, required: true
end

class Types::MutationType < GraphQL::Schema::Object
  field my_mutation, Types::MyMutationAttributes
  def my_mutation(**args)
    do_stuff(args)
  end
end

But my issue is that args will be like so:

{
  :thing: {
    :thing_id: XXX,
    :thing_data: {  }
  }
}

which does not match what my do_stuff() code expects.

As a solution, here's what I implemented:

class Types::ThatCompoundThing < Types::BaseInputObject
  argument :thing_id, String, required: false
  argument :thing_data, Types::ThatComplexThingType, required: false

  def initialize(*args, **kwarg)
    unless @thing = arg[0]["thingId"]
      @thing = arg[0]["thingData"]
    end
    super(*args, **kwarg)
  end
end

class Types::MyMutationAttributes < Types::BaseInputObject
  argument :thing_id, String, required: false
  argument :thing_data, Types::ThatComplexThingType, required: false
  def initialize(*args, **kwarg)
    arg[0]["thing"] = arg[0]["thing"].instance_variable_get(:@thing)
    super(*args, **kwarg)
  end
end

What I don't like about this solution is that I'm using an instance variable on the BaseInputObject to store the information, then I use instance_variable_get to replace the data of the element within the type of the outter object.

I guess as an improvement, I could avoid the instance_variable_get() part by defining a getter on that instance variable, but then I'd still have the issue with having code contextual to what ThatCompoundThing is within the MyMutationAttributes type.

So in the end the questions are:

  • is there a way to build ThatCompoundThing so I don't have to hack MyMutationAttributes, and that's already designed in graphql-ruby?
    • if there is, how can I do that? did I miss a part of the documentation? if not, maybe I could write that missing doc 😉
    • if there is not, would that be possible? do you think this is a legitimate use case, and a reason to implement something that could enable that?

thank you for reading this, and thank you for the project 🙌

@rmosolgo
Copy link
Owner

rmosolgo commented Aug 9, 2018

Thanks for the detailed write-up! I'm also having trouble with expressing mutually exclusive inputs. There's nothing built-in to graphql-ruby yet, so I've been solving it in plain Ruby code, for example:

def my_mutation(thing:)
  if thing[:thing_id].present? && thing[:thing_data].present? 
    raise GraphQL::ExecutionError, "Unacceptable inputs: #{thing.to_h} ..." 
  else 
    thing_value = thing[:thing_id] || thing[:thing_data] 
    do_stuff(thing: thing_value) 
  end 
end 

It has a downside of being a bit verbose, but it has the upside of being very straight-forward (it avoids messing with object internals, or integrating too closely with GraphQL-Ruby object lifecycle methods).

I'm hoping to reduce the boilerplate soon by supporting "field filters" that could help in a case like this, see: #1758 . But in the meantime, I'm pretty happy with the plain Ruby approach, because of its simplicity. What do you think about it?

@rmosolgo
Copy link
Owner

Nothing to add here, but we can keep an eye on that RFC!

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

2 participants