-
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
[Discussion] Schema Definition using the IDL #727
Comments
👍 I really want to make this a thing! Thanks especially to @f and @emilebosch for their work on the IDL-based schemas so far. Personally, I like the IDL-based approach to modifying field behavior, like # :+1:
type Author {
posts: [Post] @preloads(hasMany: "posts")
} I think making However, since the schema would "magically" map to objects in Ruby, it will be very important to expose clearly how they map together. For example, if you add something to Additionally, I think the various behavior modifications (such as authorization or
|
I'm definitely a big fan also of writing your schema in IDL, and then having a convention map it to the underlying code. Perhaps we should start by thinking about the use-case here. How is the IDL going to be written, and how will it be consumed? I think it's quite likely that these IDL's could be written (in whole or in part) by people that are not going to write the implementation. For example, it seems like for many teams (mine included), developing the schema is a collaborative process, and much of the work can happen in other environments. I think that the Apollo team said that when they built Optics, they had both front and back end engineers contributing to the schema, and they first ran it through some tool that built a GraphQL server returning mock data while the back-end team built the real implementation. That leads me to three things that I think counsel against the
The last point is really a different way of saying that it's a mixing of responsibilities - namely, it brings implementation details into the IDL. So I think I would lean more toward the decorators. Even though they're kinda gross. It makes me wish that Ruby had something like C#'s attributes: [Preloads("Author")]
public Author Author() {
return Post.Author();
} |
I think whatever loaders we have, or things that are tied to implementation should be split in a separate gem. I don't by default think its bad to annotate your schema with implementation specifics. Schemas are also used for code generation or hinting. In my PR i’v solved partially of it in a big hash map like JS graphl-tools does. Im still in doubt often between DSL and IDL. I do like writing IDL and it allows me to iterate fast on the graphql schema, and just hooking up some lambdas with a big resolver hash allows for fast prototyping of APIs. Things I like about the DSL:
Things I don’t like about the DSL:
Things I like about IDL:
Things I don’t like about IDL/Resolver map:
Some use cases I thought about:
Things that need to be decided still when going the IDL way:
Ideas? |
@theorygeek's point about compatibility with other GraphQL tools is a great one. Even if we extended the IDL for our use case, you could achieve a compatible result but going through Ruby:
But this is pretty cumbersome and having two Another option to put meaningful info in the schema is to use the descriptions for metadata, eg # @model(name: "Author")
type Author {
# @preloads(hasMany: "posts")
posts: [Post]
} |
@rmosolgo That's what i am doing my schema, but its ugly (no syntax highliting, no type checking) https://github.com/graphcool/graphql-up
Also i'd like to separate the discussion in two parts:
|
The In fact, when you pass a hash as As we build this out, I think a different # default_resolve's field resolution hook:
def call(type, field, obj, args, ctx)
# get a module for this type, eg `type Post` => `module Post`:
type_module = Object.const_get(type.name)
# get a method for the field, eg `topComments` => `Post#top_comments`
method_name = field.name.underscore
field_method = type_module.instance_method(method_name)
# call the method:
field_method.call(obj, args, ctx)
end Something along those lines would support one-type-per-file, like the DSL uses. (And since it uses proper Ruby constructs, it wouldn't be subject to Rails loading bugs.) |
Another crazy thing is that we could implement It would allow a gradual transition from DSL to the new way. |
This definitely seems worth exploring. It seems like processing that metadata could happen in application code: for example, just write instrumentation that can parse out the metadata from the description. So perhaps one option would be to provide an instrumentation class that essentially did just that? # Block is invoked for each type + field + metadata combo that was found.
instrumenter = MetadataInstrumenter.new do |type, field, metadata|
case metadata.name
when 'preloads'
# redefine the field with preloads support
end
end
Schema.define do
instrument(:field, instrumenter)
end
OK so this might be crazy, and shoot me if it is. But what if we could come up with a way to just pass those arguments to your field as normal arguments? Ruby provides all the metadata we need to do it: # Imagine that this is the field's resolver method:
def my_field(object, some_argument, some_other_argument, context:)
end
# You can ask what its parameters are:
method(:my_field).parameters
# => [[:req, :object], [:req, :some_argument], [:req, :some_other_argument], [:keyreq, :context]] Now you have enough information to map the arguments defined in the IDL to the parameters that the method accepts:
The really nice thing about this solution is that your field resolution methods are all plain Ruby, AND if there is a mismatch between your method signature and the IDL definition, it explodes during boot time instead of in the middle of a query. Plus, it's probably easier to write tests for this. You don't have to construct an |
:mind_blown:
This is amazing to me. I didn't quite see the light on GraphQL arguments -> Ruby arguments before but I think I can see it now 😆
|
I agree with this: moving that kind of implementation detail into the IDL bloats the interface with irrelevant information that makes it harder to consume. It also potentially makes it harder to expose publicly, since you now have to generate a copy of the IDL without those annotations if they contain quasi-sensitive information.
I like the breaking out of arguments, but this reads to me like C. If we go this route we should consider:
There's a lot of great conversations and ideas happening here, but to me the best argument for the DSL is still locality: it keeps all of the facts about the definition and implementation of a field in a single place. If I'm writing a resolve block/method I don't want to have to flip to another file to figure out what type it should be returning. |
I took a try at comment-based custom behavior in #789, I'd love to hear your thoughts if you get a chance to look! |
Let's imagine a world where you build your schema by writing a How do connection types work in that case? Do you write each one by hand? Or is there some way to "generate" that boilerplate? |
You're referring to things like, how do we wire up the behaviors for the paging/slicing of nodes? I think you could either annotate the field with something like: type SomeType {
# @connection
myField: SomeConnectionType
} Or we could detect connections and wire them up automatically. But that seems like a bad idea (what if I don't want the default behavior?) |
Sorry, I didn't express myself too clearly 😬 Let's say you're writing a Relay backend and you have a bunch of different object types. You'll also have connection types: type Thing {
name: String!
}
type ThingConnection {
edges: [ThingEdge!]!
pageInfo: PageInfo!
}
type ThingEdge {
node: Thing!
cursor: String!
} If you were treating
Or, something else? I looked at graphql-tools but they don't have any special support for this. |
I tried ERB 😬 #810 |
Well I've learned to love the DSL, exactly for this, and the meta typing/programming dynamic type generation. I think that IDL is cool for small projects. but it quickly starts to get difficult. Another option is to generate an But then again the logic is spread over multiple sources. Isn't there an easy way to load parts from IDL and then just |
Thanks again to everyone who participated in this discussion. After a lot of trial and error, I think #1037 will be my next take at a schema definition API! |
Discussion on a PR to auto-camelize fields recently has brought up some valid concerns:
#555 (comment)
#555 (comment)
This seems to be part of a bigger discussion on wether or not we want users write actual
graphql
, or ruby when defining a GraphQL Schema.Currently, defining a schema usually looks like this:
The resulting IDL for this, the source of truth, looks like this:
With this approach, the ruby defined schema != resulting IDL. A good thing about it is that users can write a schema without knowing the GraphQL Langage. At the same time, like discussed in the issue linked above, we don't help users learning GraphQL.
Another argument against this approach is that we're mixing the Schema / Interface with the business logic. Maybe what we want is using the IDL (the best tool we got to define a Schema) and then using plain Ruby to define the resolvers / business logic. With a proper set of conventions, this could be quite powerful. A mix of plain
.graphql
files and their ruby object counter parts.Defining a Schema using the IDL
A simple approach to this could be as simple as having something like this:
But this approach limits us in certain ways. @ Shopify, we extracted a lot of common logic into field definition arguments. For example, if a field needs to be batched or preloaded, it may look like something like this:
It's also possible to extend graphql-ruby's DSL using
accepts_definition
and Instrumenters to come up with something like this:However, with an IDL approach, we lose that DSL and we need a way to extend resolvers. What if I wanted to preload
posts
in this example?we can somehow have config at the class level to define behaviours or have some kind of before and after filters?
Doesn't seem very elegant to me ^
Just throwing ideas here, but another possible way would be to have something more like this?
Or hacky decorators?
Another approach is to instead use the IDL to define these behaviours! As discussed in graphql/graphql-spec#300. (This is not in the Spec currently, and would be a custom graphql-ruby behaviour)
There's many approaches we can take, but I just wanted to kickstart a discussion. I'm pretty excited about having the schema defined using the IDL personally, but can't seem to find the best solution for custom business logic 🤔
What do you all think❓
The text was updated successfully, but these errors were encountered: