Graphql-ruby: Release 1.8.0

Created on 4 Apr 2018  路  11Comments  路  Source: rmosolgo/graphql-ruby

This issue is to track the stable release of 1.8.0

TODO

Roughly in order:

  • [x] Merge 1.8-dev into master
  • [x] Solidify Interfaces as class or module?

    • [x] #1372 ?

    • [x] Update GitHub to the new API

  • [x] Check graphql-client compatibility
  • Fix lingering issues:

    • [x] #1388 fixed by #1391

    • [x] GraphQL-Pro route helpers fixed in 1.7.1

  • [x] Ship .rc1 I'll release another pre instead
  • [x] Review upgrade & compatibility docs, update and add new info
  • [x] Write a blog post about using the upgrader code
  • [x] Send out a "coming soon" email to the newsletter
  • [x] Address outstanding issues https://github.com/rmosolgo/graphql-ruby/milestone/51
  • [x] release 1.8.0
  • ...
  • [x] Send a stable release announcement to the newsletter

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 .define style, and updating them later.

But the usability of the class-based style is so nice, I'd recommend it!

All 11 comments

Hey @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:

https://github.com/rmosolgo/graphql-ruby/blob/44e0908354eeeb965eac0681017745c575f45864/spec/graphql/schema/object_spec.rb#L17-L20

https://github.com/rmosolgo/graphql-ruby/blob/44e0908354eeeb965eac0681017745c575f45864/spec/graphql/schema/object_spec.rb#L34-L37

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:

  • Remove def count since the implementation there is the default behavior
  • Use define_method instead of a resolve proc
  • Instead of initializing a Field instance and using add_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 馃槅

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rmosolgo picture rmosolgo  路  4Comments

sayduck-daniel picture sayduck-daniel  路  3Comments

theodorton picture theodorton  路  3Comments

jesster2k10 picture jesster2k10  路  3Comments

jturkel picture jturkel  路  3Comments