-
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
[RFC] Schema Implementation API #871
Conversation
Have you thought of ways that could enable someone to collocate definition & implementation? My fear is the context switching between a very large |
|
||
```ruby | ||
MySchema = GraphQL::Schema.from_definition( | ||
"app/graphql/my_schema.graphql", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from_definition
currently accepts the IDL as an argument, was this meant to be File.read('app/graphql/my_schema.graphql')
or were you thinking about making from_definition
support paths?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I merged this behavior separately in #872 👌
That or adding some documentation to show how one could split one large |
That'd be great. HoweverI still think that if it was possible to quickly see a definition and an implementation without too much context switching it would be great! |
Have you thought of ways to extend the schema / implementation with common behaviour? Let's take my usual |
It would still be possible to do this in the resolver function, no? i.e. def a
MyPreloader.preload(:b).then {
# stuff
}
end Maybe just a bit more verbose, but that might not be a bad thing. 🤔 |
Hmm agreed but this might get very verbose and repetitive. We've got some fields that will
That would result in possibly huge resolvers, and having it directly in the resolver wouldnt let us use Instrumenters or other ways of reusing logic across fields / resolvers |
👍 to this, but I think it can be a separate task from this one. (Make
I think it's interesting, but I'm scared of re-introducing traversal-based schema building. The current implementation has issues with Rails autoloading, cyclical dependencies, and modifying schema objects. So, it's ok with me to start small (parse a single I can think of a few ways to collocate them which can be explored later, are there other approaches to consider?
|
I like your class-method based suggestions here and in your first issue, I think something like this would be ok: # When accessing the posts field, preload the ratings
preloads posts: :ratings
def posts(first:, after:)
# ...
end I think we could implement this on top of ActiveSupport-like callbacks, for example: before_resolve posts: :preload_ratings
def posts(first:, after:)
end to call methods before/after resolving. This would be a more human-friendly version of field instrumentation. How does that sound? |
# When accessing the posts field, preload the ratings
preloads posts: :ratings
def posts(first:, after:)
# ...
end 🤔 are you saying that instead of specifying all preloads at the top of the file, you just locate these above each method? Never thought of it and it solves a problem I had which came up in the other issue. ie: This works pretty well because the number of actions in a controller, or lifecycle callbacks in a model, are pretty limited. Some controllers could have a lot of actions, but most are CRUD based. Using the same type of solution for GraphQL is a little different since it's pretty common to have a lot of fields which could quickly get out of control. |
My suggestion is a lot like controller hooks, just a different order of arguments: before_resolve :posts, :do_thing
# is like
before_action :do_thing, only: :posts It could be anywhere in the class definition. It requires a lot of hackery to make it work without an explicit method name. A nice side-effect of the explicit name is that you could apply |
end | ||
end | ||
|
||
proxy.public_send(@method_name, **method_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would probably be over-optimization at this point of development, but I wonder if eventually the call
method here could be constructed at build-time by constructing the method as a string and then instance_eval
'ing it. The idea is to avoid constructing the hash and doing this logic here - it may not even make a difference on performance, though.
Something like:
call_args = @graphql_arguments.map { |a| "#{a}: args[:#{a}]" }.join(', ')
# "letter: args[:letter]"
method_body = <<~RUBY
def call(proxy, args, ctx)
proxy.#{@method_name}(#{call_args})
end
RUBY
# def call(proxy, args, ctx)
# proxy.suit(letter: args[:letter])
# end
instance_eval(method_body)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll definitely look into it, I had a similar TODO myself :)
If it's better on the benchmarks, I'll go for it!
) | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I'm thinking about :rest
and :keyrest
args:
# :rest
def my_resolver(*args)
# => args == [{ :some_argument1 => 'some value 1' }]
end
# :keyrest
def my_other_resolver(**kwargs)
# => kwargs == { :some_argument1 => 'some value 1' }
end
A couple of thoughts on this:
- It seems like we would want to support those kinds of arguments. And if they exist, we'd basically only check to make sure that there are no positional args, and no unknown keyword args.
- Thinking through that case, if a user did use
*args
or**kwargs
, I'm not sure whether they would expect to get the special args or not. - It seems like it'd be most predictable if we could guarantee that every call into a resolver method would be done with the exact same signature. But that would require us to take a stance that either all methods will accept
:ast_node
& friends, or no methods would accept them.
That third point is really about avoiding brittle scenarios, where changing from seemingly identical method signatures ends up changing behavior. For example:
def my_resolver(some_arg1:, irep_node:, ast_node:)
end
# Several months later, someone refactors this into:
def my_resolver(**kwargs)
end
Perhaps the best option right now would be to raise an InvalidImplementationError
upon encountering a :rest
or :keyrest
parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise an InvalidImplementationError upon encountering a :rest or :keyrest parameter.
👏
def self.from_definition(string, default_resolve: BuildFromDefinition::DefaultResolve, parser: BuildFromDefinition::DefaultParser) | ||
GraphQL::Schema::BuildFromDefinition.from_definition(string, default_resolve: default_resolve, parser: parser) | ||
def self.from_definition(string, default_resolve: BuildFromDefinition::DefaultResolve, parser: BuildFromDefinition::DefaultParser, implementation: nil) | ||
GraphQL::Schema::BuildFromDefinition.from_definition(string, default_resolve: default_resolve, parser: parser, implementation: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation isn't working right now since it's hardcoded to nil
here
I'm not actively working on this branch. I decided to try experimenting with GitHub's schema a bit to see if I can find something that would work, then try it out a bit before upstreaming it. I'm worried about building something from scratch, then releasing it & applying it, but learning that it doesn't really solve our problems 😖 |
I want to provide a new API that is more familiar to Ruby developers. I hope that less DSL will make people more productive and happier 😀 See all the advantages and opinions discussed here: #727
The implementation is currently quite sloppy. But I've started writing docs and tests for what I want to build, so I'll be working towards that and cleaning up as I go. So, if anyone has feedback on those, please do share it!