-
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
Object-oriented schema definition #820
Comments
If I had to choose between the current API and this proposed one, at the beginning of the very first version of graphql-ruby, I'd definitely choose this one. I'm just not sure the migration hassle (even if it's automated) is worth it. There's so much to migrate and I haven't really faced any issues that made me wish there wasn't a "wacky" DSL. |
❤️ 🎉 🚀 Love this! I'm curious what kinds of performance hits there would be for this. I'm guessing the biggest one is going to be instantiating those objects for every field. I'm wondering about the Also, IIRC, the gem does not have ActiveSupport as a dependency. So maybe we could allow the user to inject a proc in order to fetch the correct # I scope-creeped a `define` API into my comment here :p
Schema = GraphQL::Schema.define_from_idl do
source_file 'app/graphql/schema.graphql'
# Plug in your own behavior:
resolve_object -> (type) { "Objects::#{type.name}".safe_constantize }
end The challenge with overriding that behavior is that it has to be two-ways, but that's not really a problem; the user would have to override both |
One additional con for the list, which is common to most IDL-based approaches: it spreads out the definition of a field across multiple files. |
This might be a terrible idea, but what about defining the IDL within the Ruby file? Something like: MySchema.define_type_from_idl(<<-'GRAPHQL'
type Post {
title: String!
body(truncateAt: Int): String!
comments(moderated: Boolean = true): CommentsConnection
}
GRAPHQL)
class Types::Post < GraphQL::BaseObject
# This is default, inherited behavior:
# def initialize(object, context)
# @object = object
# @context = context
# end
# This is default field resolution behavior
# def title
# @object.title
# end
end
|
or even something like: class Types::Post < GraphQL::BaseObject
definition <<~GRAPHQL
type Post {
title: String!
body(truncateAt: Int): String!
comments(moderated: Boolean = true): CommentsConnection
}
GRAPHQL
end |
I ❤️ @arthurschreiber's DSL above, but I'd also love the flexibility to do either. I appreciate being able to look at a One my favorite aspects of this new DSL is how easily it will be to mix in modules to add in custom helpers, fields, interfaces, etc. |
@cjoudrey , @arthurschreiber & @bswinnerton I think there must really be something to the GraphQL-in-Ruby approach, we also talked about it a bit here. Here's the bridge that I couldn't cross: how do you determine the whole schema in that scenario? For example, what (pseudo-)code goes here: # app/graphql/my_schema.rb
# From .graphql files:
# MySchema = Schema.from_definition(glob: "./**/*.graphql")
#
# OR:
# From .rb files:
MySchema = # ??? (A bonus is something that plays well with Rails autoloading.) |
@eapache thanks for pointing out that extra consideration about splitting the source of truth for field definitions, I've updated the issue description so new readers will find it quickly. (Maybe we'll find a GraphQL-in-Ruby approach that mitigates that downside.) |
@theorygeek regarding implementation, I had something very similar in mind! Perhaps an API like: MySchema = GraphQL::Schema.from_definition(
glob: "app/graphql/**/*.graphql",
# Specify a Ruby namespace where objects can be found:
default_resolve: Objects,
) The Performance-wise, the issue is making one of these proxies for each object in the response. We could make a corresponding win however. Since this API doesn't support field-level context, we might be able skip the |
@timigod I'm glad to hear you don't mind the current API too much 😊 There are a few ways that it makes us less productive at GitHub:
There are ways to mitigate these issues (for example, implementing a layer on top of graphql-ruby's DSL), but graphql-ruby doesn't guide people toward the "pit of success"! |
At Shopify we have a layer on top with an API that looks very-very similar to the DSL, except implemented in a class-based way instead of using blocks. It solves a lot of the pain points without changing the usage too much. |
A question from
class Types::Post
# field-level flag
visibility :internal, fields: [:moderation_reason]
end
def instrument(type, field)
if type.implementation_class.visibility[:internal].include?(field)
field.redefine(...)
else
field
end
end (Maybe we could write some sugar to make this easy)
|
I could be missing something super obvious here, but couldn't we: 1- Start from the schema roots
Another option could be custom directives, i.e. class Types::Post < GraphQL::BaseObject
definition <<~GRAPHQL
type Post {
moderationReason: String @visibility(type: "internal")
}
GRAPHQL
end You'd want a way to mask these directives from the outside world though, i.e. people would see: type Post {
moderationReason: String
} This probably isn't a one-size fits all solution though. |
@cjoudrey traversing the schema from entry points could totally work! Personally, I was hoping to leave
I like customizing via
However, I didn't recommend
One advantage is that But personally, I'm still on the fence! |
@eapache that sounds awesome |
Yeah, to be honest I also prefer leaving the rest out of the IDL. re: #820 (comment) -- I can't put my finger on it, but there's something I dislike about having to use instrumentation. Could an API similar to Rails class Types::Post < GraphQL::BaseObject
definition <<~GRAPHQL
type Post {
moderationReason: String
}
GRAPHQL
before_resolve :user_is_internal, fields: [:moderation_reason]
end You'd need a way to let |
😯 whoa .... 🤔 |
💯
We're thinking about ditching a bunch of it because it's a lot to maintain, but tbh we still haven't come up with anything better. Just writing |
Looking good! With respect to step 4, "put shared logic in an By way of an example, take this alternative to having # app/graphql/types/post.rb
class Types::Post < GraphQL::Object
def viewer_is_author?
viewer == @object.author
end
private
def current_user
# something like
SessionObject.new(context).current_user
end
end |
@malandrina I totally share your concern that a root object will become bloated! Here's the part I can't figure out how to do:
What kind of Ruby API can express that preference? As soon as you start using (I tried to express that preference by using a function-based API, but ... here we are two years later 😆) Unrelated note, but here are some references to previous requests for class-based API: |
@rmosolgo thank you for linking to those older conversations! You have clearly put a lot of thought into how a preference for composition over inheritance might be expressed in graphql-ruby's API. Your comment #122 (comment) is edifying. 👍
This is true. I think the best one can do is expose an "opinionated" public API that serves as an example/expresses a preference for how to share behavior and enforce separation of concerns. But the value of such an approach is questionable because, Ruby being Ruby, users are going to go ahead and extend and monkeypatch when they want to, regardless of whatever perfect architecture you've established. 🤷♀️ tl;dr sounds like you've given a lot of thought to this and previous experiments with a "class-driven" approach were unsuccessful, so I trust your judgment that sharing behavior via |
I wouldn't quite go that far, or at least ... I would say, it's a familiar option. For people who want to get it done quickly, they can put all shared logic in the god class at the top of the hierarchy. It's not the most maintainable solution, but it's easy 😬 Then, if/when people want a different arrangement, they can extract and reshuffle the same way we do for So in that way, I wouldn't say it's really the best, but I'm hoping to make a different tradeoff so that folks can get started and be productive with graphql-ruby more quickly. |
On the GraphQL-in-Ruby bit, it seems like:
But the biggest advantage I see to the IDL is that it can be shared between projects, and it is the true source of truth. The GraphQL-in-Ruby paradigm breaks that, because now you really do need to go back to doing
|
If what we really want with the GraphQL-in-Ruby is to reduce the amount of switching between files when you're trying to implement the schema, we could accomplish that with some tooling similar to the
|
In a previous issue I gave the example of extending fields with custom arguments such as They won't go in the IDL, since the user should not be aware of those "extensions": type Post {
title: String!
body(truncateAt: Int): String!
comments(moderated: Boolean = true): CommentsConnection
} But if they have to be somewhere in the "resolver" class, where should they go? on the class? Magic decorators? class Types::Post < GraphQL::BaseObject
preload :an_association, on: :body
def body(truncate_at: nil)
...
end
end I'm afraid if these extensions are on the class, we're now adding a third place to look at when defining fields. EDIT: i now realize i missed this comment: #820 (comment) |
My 2c: I'd be happy to move to a class-based definition, but why not keep the field definition? i.e. class Types::Post < GraphQL::BaseObject
field :author, AuthorType # resolves object.author
field :titleSlug, StringType # resolves object.title_slug
field :body, StringType, meta: :data do |argument:, **keywords:|
decorator.formatted_body(keywords)
end
field :isRead, BooleanType do
current_user.read_posts.include?(object)
end
private
def current_user
# @context is set in #initialize
context[:current_user]
end
def decorator
# @object is set in #initialize. One might also trivially do
# @object = object.decorate in a custom initializer, and always refer to this
object.decorate
end
end This would be similar to how You get some of the advantages of the original proposal
Plus you keep the ease of use for custom field metadata, and have a trivial code update path. The problem of stacktrace and debuggability is also still reduced, since there is no complex magic DSL (we only need Of course, this is worse for people who like their definition in IDL, which I am not a big fan of :) |
@riffraff thanks for bringing that up! We had discussed an API like that as well, because many Ruby libraries have one (for example, MongoMapper or Sequel). Here are some pros and cons that I saw:
I think I understand this point:
But, can you help me understand some of these points? I want to keep these concerns in mind but I don't quite understand it yet:
I want to "cover all the bases", so I don't want to miss out on these considerations! |
forgive me if I wasn't clear, I meant that this points are shared advantages in both object-based approaches (with and without IDL) compared to the current pure-DSL-with-types-but-not-classes definition. You have good points about mutually referential types and arguments, I didn't put any thought into this and mostly made a strawman proposal. Anyway, possibly, we could just refer all types as As for arguments, I guess you could abuse default values to specify types, which seems nasty, or define them as metadata
FWIW, we could keep 100% of the current field DSL, just with classes around them instead of |
@riffraff's proposal looks a lot like what we have at Shopify right now.
Rails autoloading has worked well for us on this point with only the occasional need for a manual
We do approximately the following for complex fields: field :foo, Type do |field|
field.argument :name, Type
field.resolve = lambda do |obj, args, ctx|
end
end |
I haven't read all the comments yet; but I think I like where this is going. Another really interesting direction this could go is something similar to the GraphQL-compose library in ruby. https://github.com/nodkz/graphql-compose What makes that approach interesting is that it allows you abstract the shared logic as small building blocks; then use them to compose the schema. Examples of these might be a set of 'finder' modules(findOneById, findWhere, etc) which know how to do advanced filtering for which ever datastore you're using. Refer to the mongo and elasticsearch libraries built using this pattern; it's really impressive how much he was able to do with so little code. The other really neat thing it does is compose types via the GraphQL syntax, inline with the rest of the implementation. That syntax makes creating things like enums very straightforward. I'm typing this on my mobile from the subway, so I can't give samples unfortunately. I'll follow up if anyone else is intrigued by this. I'm all for nice a nice DSL, but it has to be built on a solid foundation. I see huge opportunity here to leverage the patterns outlined above; then slap a nice DSL on top of it. |
Something I'd like to mention that I haven't seen in here explicitly is how perspective approaches will handle (or at least behave) in terms of development and debugging. Recently I got treated with an autoload error about my root QueryType because I misspelled the name of a mutation field in MutationType 😱 This is one of the reasons why I have 👍 s all over @riffraff 's suggestion and the ruby/inheritance approaches in this thread. I feel like being structured in that way would allow for more useful information in errors and stack traces. /my 2 cents |
Will |
Slowly but surely, I'm getting started at #871 :) |
Closed #871 in favor of tinkering a bit with GitHub's schema, I'll be back with another when we find something promising :) |
"Closed" (ok, just barely started) by #1037 |
Here's a proposed new API for defining GraphQL behaviors with Ruby. If this appeals to you or doesn't appeal to you, please chime in. Otherwise, I will probably implement it :)
.graphql
files containing type definitions.For example:
.rb
file containing a Ruby class:By default, this object is a proxy. If you don't need custom behavior, you can bypass this object altogether.
ApplicationObject
:graphql-ruby
calls methods on these objects to resolve fields..graphql
files?.rb
and.graphql
filesPostConnection, PostEdge, CommentConnection, CommentEdge
, same problem as any DSL-based approach)The text was updated successfully, but these errors were encountered: