This issue is to track the stable release of 1.8.0
Roughly in order:
.rc1pre insteadHey @rmosolgo, first of all, thank you for your work on graphql-ruby!
We're using it in a new project, and I've been wondering: should we start with the current pre-release of 1.8.0 right away, or should we stick to 1.7.x and wait until the first RC or so? Thanks!
If it were me, I'd start on 1.8. The only big caveat is Interface types -- I might switch up the current implementation so that they're Ruby modules. So you could handle that by either writing them as classes and taking the hit to redo them if they change, or writing them in the old .define style, and updating them later.
But the usability of the class-based style is so nice, I'd recommend it!
Our project isn't going to be terribly large, so a certain amount of refactoring to keep up with 1.8's development is perfectly acceptable. Thanks for the heads-up!
@rmosolgo I'll echo the thanks for all your hard work, the 1.8 changes are really great.
I'm starting to migrate a project over to the class-based API, and I'm trying to decide on if/how to change the way we're dynamically generating types. I think there are a number of ways it could be done (including leveraging #to_graphql), but I'm wondering if you've had any ideas yet about what an approach that doesn't use GraphQL::ObjectType.define might look like, or if that's even worth trying right now.
I think it will be Class.new(BaseObject) { graphql_name(generated_type_name) ... }, what do you think of that?
Edit: there are some trivial and incomplete examples in the specs, for example:
Makes sense! We also add fields dynamically, and I initially thought I might be able to use class_eval to add the resolve method, but I'm not sure if there's a way to get the dynamically-created class in the Field instance. Here's what I'm playing with at the moment:
Factory:
class Graph::Factories::Metric
def self.define(metric)
if Graph::Objects::Interval.fields[metric.source].nil?
return_type = Graph::Objects::PostSource.create(metric.source)
field = Graph::Fields::PostSource.new(metric.source, return_type,
owner: Graph::Objects::Interval, null: false)
Graph::Objects::Interval.add_field(field)
end
end
end
Field:
class Graph::Fields::PostSource < Graph::Fields::Base
def initialize(*args, **kwargs, &block)
source = args.first
resolve = -> (obj, args, ctx) { obj.where(source: source) }
super(*args, resolve: resolve, **kwargs, &block)
end
end
Return Type:
class Graph::Objects::PostSource < Graph::Objects::Base
def self.create(source)
Class.new(self) do
graphql_name "#{source.classify}Posts"
end
end
field :count, Integer, null: false
def count
@object.count
end
end
So the one part still relying on the pre-1.8 API is the resolve proc. I suppose I could call class_eval on the return type class directly in the factory, but subclassing field this way and defining resolve there made more intuitive sense to me.
Could you share an example of how Graph::Factories::Metric.define is used? Just curious to get a sense of it.
Right now I'm just calling it from Graph::Objects::Interval (Metric is an AR model):
class Graph::Objects::Interval < Graph::Objects::Base
Metric.all.each do |metric|
Graph::Factories::Metric.define(metric: metric)
end
end
Ideally though I think I want to be able to call .define when I create Metric record(s) in a test, and have the fields and types be added so that I could verify them in a GraphQL test. I also probably want to call it from an initializer (rather than from Interval) in dev and prod.
Thanks for sharing. I tried to understand it by inlining a lot of the code. Then I made a few changes:
def count since the implementation there is the default behaviordefine_method instead of a resolve procadd_field, use the field(...) helper In my case it turned out looking like this:
class Graph::Objects::Interval < Graph::Objects::Base
# Read some objects from the database
Metric.all.each do |metric|
# Generate a field from each object, using `source` as the name
field_name = metric.source
# Don't override an existing field
if fields[field_name].nil?
# Generate a return type for this field. It has a `count` field.
return_type = Class.new(Graph::Objects::Base) do
graphql_name "#{field_name.classify}Posts"
field :count, Integer, null: false
end
# Define a field with the same name, returning the generated return type
field(field_name, return_type, null: false)
# Implement the field with a method
define_method(field_name) do
@object.where(source: field_name)
end
end
end
end
Like you said, maybe for your app you want to reorganize it a bit to fit your flow, but I thought it was a fun exercise and maybe a fresh perspective would come in handy! Thanks again for sharing your use case a bit.
Thanks! The inline approach is pretty much where I started. 馃槃
I just realized I could use irep_node to infer the field name in a resolve method directly on Interval:
class Graph::Factories::Metric
def self.define(metric:)
field_name = metric.source
if Graph::Objects::Interval.fields[field_name].nil?
return_type = Graph::Objects::PostSource.create(field_name)
Graph::Objects::Interval.field(field_name, return_type, null: false,
method: :resolve_post_source, extras: [:irep_node])
end
end
end
class Graph::Objects::PostSource < Graph::Objects::Base
def self.create(source)
Class.new(self) do
graphql_name "#{source.classify}Posts"
end
end
field :count, Integer, null: false
end
class Graph::Objects::Interval < Graph::Objects::Base
def resolve_post_source(irep_node:)
object.where(source: irep_node.name)
end
end
I'm pretty happy with this approach鈥攏o need for separate Fields, and it eliminates any potential confusion around what's in scope in define_method or class_eval, since the resolver in the usual place.
I think the only slight improvement might be a way to get field_name without using irep_node... extras might be the only thing that feels slightly out of place to me in the new API.
Cool! The only thing I would say is to use irep_node.definition_name instead. If the query uses a field alias, then .name will return the alias, not the real field name. extras: _is_ a bit of a hack ... but it'll do for now 馃槅
Most helpful comment
If it were me, I'd start on 1.8. The only big caveat is Interface types -- I might switch up the current implementation so that they're Ruby modules. So you could handle that by either writing them as classes and taking the hit to redo them if they change, or writing them in the old
.definestyle, and updating them later.But the usability of the class-based style is so nice, I'd recommend it!