-
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
An idea for dynamically connecting GraphQL types & queries to ActiveRecord #945
Comments
One surprising side-effect of this approach is that the problem of reloading changes when using an interface (#929) appears to go away. |
@AndyKriger This looks great! Would this make it possible for the model to have multiple |
That seems like it would work. If you passed a different name (instead of relying on the default name), you'd get a different type. What is a scenario where you would want to do that? I've refined the design since I posted this original idea to define include/exclude fields and to handle has_many associations... # used to create a GraphQL type from a declarative specification
# @param name [String] the name of the type, defaults to the class name
# @param description [String] a docstring for GraphQL
# @param include [Array<Symbol>] a list of fields to expose with GraphQL; if not passed, all fields are exposed otherwise only declared fields are exposed
# @param exclude [Array<Symbol>] a list of fields to hide from GraphQL; if both include & exclude are passed then excluded fields take priority
# @return [GraphQL::ObjectType] a GraphQL object type to declare for the schema
def graphql_type( name: self.name,
description:'',
include: [],
exclude: [])
# because of the way GraphQL creates objects
# we have to reference self outside of the define block
columns = self.columns_hash
belongs_to = self.reflect_on_all_associations(:belongs_to)
has_many = self.reflect_on_all_associations(:has_many)
define_singleton_method(:graphql_type) do
GraphQL::ObjectType.define do
name name
description description
# this is a list of fields that never get returned
# we remove the primary key and any foreign key columns (relationships are handled later)
db_fields_never = ( belongs_to.map(&:association_foreign_key) + belongs_to.map(&:association_primary_key) ).uniq.map(&:to_sym)
# figure out which database fields we are exposing
db_fields = (include.empty? ? columns.keys : include).map(&:to_sym) - exclude - db_fields_never
# create GraphQL fields for each exposed database field
db_fields.each do |f|
field f, GraphQLType.convert_type(columns[f.to_s].type)
end
# a type might refer to other types (ex: an Application has many Merchants and a Merchant has many Principals)
# however, a database association might not have all it's models defined as GraphQL types
has_many.each do |reflection|
typename = "#{reflection.class_name}Type"
field reflection.name, Types.const_get(typename).to_list_type if Types.constants.include? typename.to_sym
end
end
end
end |
Thanks for sharing this cool idea! We definitely need something (or several options) like this for graphql-ruby. GraphQL::Models is something similar, I know Shopify has an in-house wrapper also, and we're currently trying out something like that for GitHub. Similar conversations about improving the graphql-ruby schema definition experience are in #820 #871 #727. So we're all heading in the same direction :) When it comes to changing the gem's API, I want to make sure that it's useful for non-Rails users, too. For example, there might be a flexible API at the bottom with a layer of Rails-oriented helpers on top. |
I looked at the activerecord gem. There are lots of great ideas in that gem, but I wanted something with less boilerplate, less DSL, that lived within the Model classes themselves; as implemented, this is oriented towards the project I'm working on. |
Shopify's AR/model integration is almost the opposite of this issue. We integrate it into the GraphQL type rather than the AR model. Within a GraphQL type definition, we just set the model its "backed" by which enables features like Relay's global identification, caching, etc. While this example is great for dynamically creating types easily, I personally feel it splits up your GraphQL type definitions and obscures them a bit. |
Good point, the approach is from the opposite direction. But what I mean is, the problem we're trying to solve is the same: the current DSL makes it hard to implement a schema quickly! I agree with your point about organization. In my experience, Rails models are already too busy; I don't want to add anything else to those files. I would rather have a second file which draws from the model (like @swalkinshaw described). But if you prefer a model-based approach, it's interesting to explore it, like the example above. |
I can see the argument for separation of concerns. It's a style choice; for me, I prefer to see all my model-related concerns in one place. For example, in one place, I see AR validations & hooks as well as the declarations of what will be exposed via GraphQL and how. Right now, I'm working on extending the idea to generating basic CRUD easily rather than repeating the same pattern across many model classes. Most of the time, it feels like model interactions are fairly straightforward so I want a way of getting that up-and-running quickly and without a lot of repetitive boilerplate. As I said before, this is a work-in-progress on a new project so I'm experimenting as I go. |
Very cool @AndyKriger , I like it. The thought of hand coding all of our types in an app with over 100 models was a daunting task. I've added a bit to your original code to accommodate all relations (except polymorphic relations) and provide some rudimentary fallbacks if a relation is invalid. Have you started a github project on this? I'd be interested in contributing since I've some ideas for additional extensions in mind. BTW, this is running under Rails 3.2 with Ruby 2.2.8
|
I haven't started a Github project for it. For one, it's very much a work in progress with the needs of the project it's part of (which is in it's early stages) and has changed a bit since this. Also, it's internal code that I shared as a proof-of-concept. I'll discuss it with my team and see if I can make it into something more public. Would be happy to collaborate either way. |
Thanks again for sharing this cool idea, if you end up publishing anything, please add it to our list of related projects: http://graphql-ruby.org/related_projects.html ! |
(feel free to keep chatting here, but i want to close this issue so I can keep track of to-do items on this project!) |
@AndyKriger FYI, I've decided to run with your original idea and have pretty much wrapped up some functionality. I'll be structuring the code a bit more and gemifying it in the future. I'll be sure to give you credit for the original idea. I've done some rudimentary testing with the following code under Rails 3.2 and 5.0. I've no reason to think that it wouldn't work under 4.0 as well but will test more as I move forward to a gem. Here is what I have for now. It's a bit messy since it is all crammed into one class, but I'll be breaking out modules as I move it into a gem. It was easier at this point to keep it in one initialization file. Currently models are not instrumented for graphql generation of their corresponding graphql types unless they have their respective attribute tags present on the model for the generators: graphql_query There is an optional: I've also plugged in some optional authorization of the types using meta tags when they are generated. Currently it is using an ability method on the current_user using cancan, but does a soft scan to bypass the authorization if the method does not exist. It does use the same method GraphqlType.authorized? to determine when nested associations are generated based on the presence of the public method on the model. I've tested it under Rails 5 by pointing to an existing database, doing a dbdump and then generating the models using https://github.com/frenesim/schema_to_scaffold>. I then adding the graphql gem, graphiql gem and configured the necessary routes and controller. Then dropped in my initializer and configured the models with the tagged attributes. Everything was fairly easy to get setup and running with a generic graphql interface for the 100+ rails models. With the exception of the create resolver, the other default resolvers appear to work in a generic manner that allows them to be used on all models. As with your example, the default resolvers can still be overridden in the definition on the model. I've also created a method to determine the correct includes (and references in Rails 4+) to be used with deeply nested association queries that allows them to be retrieved in one batch rather than in (n+1) queries. The type output nesting can be configured to return :flat, :shallow, or :deep nesting based on the relay specification by setting a Module variable @@connection_strategy (bad name, will change it in the future). In addition the output naming can modified using :underscore or :camelize by setting the module variable @@type_case. This is most certainly a WIP and not up to the standards of general good code practices. But I am happy with the functionality that I've been able to support up to this point. config/initializers/graphql_type.rb
app/graphql/graphqltype_schema.rb
Gemfile
app/controllers/graphql_controller.rb
|
@geneeblack @AndyKriger I'd also be interested in helping build and contribute to this gem. I've just copied this code into my own project, and got it working. I had to make some tweaks because my project is running mongodb/mongoid, such as changing It's a shame graphql-rails is taken and abandoned, because I think that would be a great name for it. Maybe we can reach out to him when the time comes and see if he'd be willing to give up the name. Or we can come up with our own name. Let me know when you have a repo set up, and we can start making issues to track the work. edit: Actually, it looks like @jamesreggio is active on GH. Maybe he can chime in here on whether he'd be willing to give up this gem name? |
@dkniffin I've published a gem https://rubygems.org/gems/graphql_model_mapper/versions/0.0.5 This is my first attempt at publishing a gem, so any feedback/guidance will be appreciated. The repo is at https://github.com/geneeblack/graphql_model_mapper If you have some ideas on how this could accommodate mongodb I'd be willing to hear them. I don't currently have any projects set up with mongodb so I don't have any readily available test cases to work with. |
@geneeblack I'll take a look. Just so you know, your links aren't working right. If I copy/paste the url into the browser, it works great, but clicking the link sends me to https://github.com/rmosolgo/graphql-ruby/issues/url |
@dkniffin Thanks, I changed the links to straight addresses rather than using the github link markup. they should be working now. |
i'm interested in helping out with this - i have a done a lot of cleanup and improvement to my initial GraphQLType code and i got approval to put this code out into the world |
@AndyKriger very cool, your help is greatly appreciated, especially since you fostered the original idea, I apologize for the state of the code in advance since ruby/rails is not my native coding language. I'm sure your improvements will bring some needed order to the project. |
i'm taking the discussion over to the graphql_model_mapper project where i posted the most recent iteration of that module |
@AndyKriger Check out https://github.com/keepworks/graphql-sugar. It is basically a wrapper over |
In a Rails application with ActiveRecord model objects, the information that would be exposed via GraphQL is already there in the model. Using macros, the ActiveRecord model could be declaratively & dynamically connected to GraphQL's type & query declaration to reduce the amount of redundant code. This is a work-in-progress and probably has much room for improvement. Not everything available in the GraphQL DSL is supported yet though the direction is straightforward. There's also room for using hooks in the module macros to let model objects customize behavior further.
Feedback on this approach would be appreciated...places for improvement or refinement, caveats, hidden gotchas (because of Ruby or Rails that have not been considered or accounted for).
Thank you
The text was updated successfully, but these errors were encountered: