-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Make a nice API for composition #91
Comments
Perhaps this is because a simple example has been used for pedagogical purposes but it seems to me that types currently support this scenario just fine. That said I have run into scenarios where it would be useful to be able to define resolve methods on an interface's fields (perhaps that can be optionally overridden by the implementing type). Would that cover the use cases supported by mixins while also keeping the number of concepts small? |
That's an interesting point! Use "subtypes" to capture that shared behavior. I share your desire to keep the number of concepts small. Maybe a suitable solution to this issue would be to add a guide for "Code Reuse Patterns" That example is a bit contrived, here's a real-life example: With "mixin"HasCheckInCounts = GraphQL::Mixin.define do
field(:total_count, types.Int)
field(:guest_count, types.Int)
field(:regular_count, types.Int)
field(:volunteer_count, types.Int)
end
EventType = GraphQL::ObjectType.define do
mixin(HasCheckInCounts)
end
LocationType = GraphQL::ObjectType.define do
mixin(HasCheckInCounts)
end With a moduleThis is how I've been solving problems like this lately. There's nothing pretty about it but I like it because it uses plain-Ruby concepts and the API gives plenty of room for customizing or changing the implementation later on. module CheckInCountsFieldsFactory
COUNT_FIELDS = [
:total_count,
:guest_count,
:regular_count,
:volunteer_count,
]
def self.create(object_type)
COUNT_FIELDS.each do |field_name|
object_type.field(field_name, object_type.types.Int)
end
end
end
EventType = GraphQL::ObjectType.define do
CheckInCountFieldsFactory.create(self)
end
LocationType = GraphQL::ObjectType.define do
CheckInCountFieldsFactory.create(self)
end With a typeThis solution is so nice because it uses GraphQL basics. I like the flexibility provided by the The only thing I don't like is how the GraphQL structure diverges from the ActiveRecord structure. That is, fields that used to be on CheckInCountsType = GraphQL::ObjectType.define do
field(:total_count, types.Int)
field(:guest_count, types.Int)
field(:regular_count, types.Int)
field(:volunteer_count, types.Int)
end
EventType = GraphQL::ObjectType.define do
field(:check_in_counts, CheckInCountsType) do
# pass in the Event itself, since the methods are all on the Event
resolve -> (obj, args, ctx) { obj }
end
end
LocationType = GraphQL::ObjectType.define do
field(:check_in_counts, CheckInCountsType) do
resolve -> (obj, args, ctx) { obj }
end
end |
This would be a big breaking change, but what if types were classes instead of instances, w/ class methods used for definition. That way we could do something like: module CheckInCounts
extend ActiveSupport::Concern
included do
field :total_count, types.Int
field :guest_count, types.Int
field :regular_count, types.Int
field :volunteer_count, types.Int
end
end
class EventType < GraphQL::ObjectType
include CheckInCounts
field :name, types.String
field :date, types.Int
end
class LocationType < GraphQL::ObjectType
include CheckInCounts
field :name, types.String
field :address, AddressType
end and could use inheritance like: class VolunteerType < UserType
field :numberOfHoursVolunteered, types.Int
end
class GuestType < UserType
field :guestOf, UserType
end Is there some reasoning behind An added benefit (actually the reason I started thinking about this) is that we could break long type specific field definitions into their own classes under the namespace of a type. I have been ending up w/ some 30-40 line resolve procs in complex types that feel like they deserve their own files and unit tests - I think something like this could make that easier. # type w/ long resolve
class EventType::Guests < GraphQL::Field
type types[UserType]
argument :order, types.String
argument :role, types[UserRolesEnumType]
argument :minNumberOfEventsAttended, types.Int
# complex code specific to arguments and context goes here
def resolve(obj, args, ctx)
# ....
end
end
class EventType < GraphQL::ObjectType
field :guests, field: EventType::Guests.new
end |
I definitely agree that we need some clear paths for breaking up large type definitions! Configuration like this is partly supported: field :guests, field: EventGuestsField I say partly because I think the incoming field is mutated during the Types as Classes?Before the GraphQL spec was released (and there was only some passing mention of the idea from ReactConf), I worked up an "implementation" where types were classes which "wrapped" the object they exposed. I guess it wasn't just the class-driven aspect of that structure that seemed bad, but it had lots of problems:
Anyways, that's why I departed from the class-based type system. Maybe we did lose something though, since we're so used to sharing behaviors through inheritance! |
I like how your Is this something that could potentially be solved by graphql/graphql-spec#48 ? |
Interface is nice for publishing your API, but AFAIK it doesn't actually implement fields for you. You still have to say |
The issues I have with mixins rather than default implementations for interfaces are:
In detail: Interfaces being able to provide a default implementation is functionally equivalent to mixins and therefore can support all the same use cases. The disadvantage of mixins is that while your type now conforms to an implicit interface, that is not communicated through the graph so useful information is being thrown away. To consider your original example You won't always need to have the interface of a mixin exposed in the graph but it seems a shame to throw away potentially useful information that could have been available for free. Regarding your comment above: Two types exposing the same fields conforming to an interface, but resolving differently, can be handled if an interface's default resolves can be overridden in implementing types. I believe this is how interfaces work in |
All good points about interfaces with default implementations, you get the benefit of |
Thanks for the great discussion, I merged a try at using interfaces as mixins #108 |
What if you have a bunch of types with the same fields? I've been passing the objects into functions and adding fields to them, but it would be good to have a proper API for that. One idea I have is a "mixin", like:
The text was updated successfully, but these errors were encountered: