├── .gitignore ├── LICENSE ├── README.md ├── Rakefile ├── graphql-rails-generators.gemspec └── lib ├── generators └── gql │ ├── USAGE │ ├── gql_generator_base.rb │ ├── input_generator.rb │ ├── model_search_base_generator.rb │ ├── model_search_generator.rb │ ├── model_type_generator.rb │ ├── mutation_generator.rb │ ├── mutations_generator.rb │ └── templates │ ├── create_mutation.rb │ ├── delete_mutation.rb │ ├── model_mutation.rb │ ├── model_search.rb │ ├── model_search_base.rb │ └── update_mutation.rb └── graphql-rails-generators └── version.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Sharp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-rails-generators 2 | 3 | A few generators to make it easy to integrate your Rails models with [graphql-ruby](https://github.com/rmosolgo/graphql-ruby). I created this because I was wasting too many keystrokes copying my model schema by hand to create graphql types. 4 | 5 | This project contains generators that look at your ActiveRecord model schema and generates graphql types for you. 6 | 7 | - `gql:model_type Post` - Generate a graphql type for a model 8 | - `gql:input Post` - Generate a graphql input type for a model 9 | - `gql:mutation Update Post` - Generate a graphql mutation class for a model 10 | - `gql:search_object` - A search object based on [SearchObjectGraphQL](https://github.com/RStankov/SearchObjectGraphQL) 11 | 12 | ## Installation 13 | 14 | ``` 15 | gem 'graphql-rails-generators', group: :development 16 | ``` 17 | 18 | ## Requirements 19 | 20 | This library only supports ActiveRecord, though it would be fairly trivial to add support for other ORMs. 21 | 22 | ## Usage 23 | 24 | ### gql:model_type 25 | 26 | Generate a model type from a model. 27 | 28 | ``` 29 | $ rails generate gql:model_type MODEL_CLASS 30 | ``` 31 | 32 | #### Options 33 | 34 | - `--name` - customize the file/class name, useful if you don't want the default Type suffix. 35 | 36 | #### Example 37 | 38 | ```ruby 39 | # app/graphql/post_type.rb 40 | module Types 41 | class PostType < Types::BaseObject 42 | field :id, GraphQL::Types::ID, null: true 43 | field :title, String, null: true 44 | field :body, String, null: true 45 | field :created_at, GraphQL::Types::ISO8601DateTime, null: true 46 | field :updated_at, GraphQL::Types::ISO8601DateTime, null: true 47 | end 48 | end 49 | ``` 50 | 51 | ### gql:input MODEL_CLASS 52 | 53 | Generate an input type from a model. 54 | 55 | ``` 56 | rails generate gql:input Post 57 | ``` 58 | 59 | #### Options 60 | 61 | - `--name` - customize the file/class name, useful if you don't want the default Input suffix. 62 | 63 | #### Example 64 | 65 | ```ruby 66 | # app/graphql/types/post_input.rb 67 | module Types 68 | module Input 69 | class PostInput < Types::BaseInputObject 70 | argument :title, String, required: false 71 | argument :body, String, required: false 72 | end 73 | end 74 | end 75 | ``` 76 | 77 | ### gql:mutations MODEL_CLASS 78 | 79 | Generate create, update and delete mutations for a model. 80 | 81 | ``` 82 | rails generate gql:mutations Post 83 | ``` 84 | 85 | #### Example 86 | 87 | ```ruby 88 | # app/graphql/types/post_input.rb 89 | module Types 90 | module Input 91 | class PostInput < Types::BaseInputObject 92 | argument :title, String, required: false 93 | argument :body, String, required: false 94 | end 95 | end 96 | end 97 | ``` 98 | 99 | ### gql:mutation MUTATION_PREFIX MODEL_NAME 100 | 101 | Generate a mutation class from a model. 102 | 103 | A quick note about the mutation generator... 104 | 105 | The mutation generator generates something akin to an "upsert" mutation. It takes two arguments: an optional `id` and an optional `attributes`, which is the input type for the model. If you pass an `id`, it will attempt to find the model by the `id` and update it, otherwise it will initialize a new model and attempt to save it. 106 | 107 | ``` 108 | rails generate gql:mutation Update Post 109 | ``` 110 | 111 | #### Example 112 | 113 | ```ruby 114 | # app/graphql/mutations/update_post.rb 115 | module Mutations 116 | class UpdatePost < Mutations::BaseMutation 117 | field :post, Types::PostType, null: true 118 | 119 | argument :attributes, Types::Input::PostInput, required: true 120 | argument :id, GraphQL::Types::ID, required: false 121 | 122 | def resolve(attributes:, id: nil) 123 | model = find_or_build_model(id) 124 | model.attributes = attributes.to_h 125 | if model.save 126 | {post: model} 127 | else 128 | {errors: model.errors.full_messages} 129 | end 130 | end 131 | 132 | def find_or_build_model(id) 133 | if id 134 | Post.find(id) 135 | else 136 | Post.new 137 | end 138 | end 139 | end 140 | end 141 | ``` 142 | 143 | ### gql:search_object MODEL_NAME 144 | 145 | Generate a search object from a model using [SearchObjectGraphQL](https://github.com/RStankov/SearchObjectGraphQL) 146 | 147 | If you have not yet created a base search resolver: 148 | 149 | `rails g gql:model_search_base` 150 | 151 | \*_Adds `gem 'search_object_graphql'` to gemfile_ 152 | 153 | #### Example 154 | 155 | ```ruby 156 | # app/graphql/resolvers/base_search_resolver.rb 157 | module Resolvers 158 | class BaseSearchResolver < GraphQL::Schema::Resolver 159 | require 'search_object' 160 | require 'search_object/plugin/graphql' 161 | include SearchObject.module(:graphql) 162 | end 163 | end 164 | ``` 165 | 166 | Then generate a search object for your model: 167 | 168 | `rails g gql:model_search Post` 169 | 170 | #### Example 171 | 172 | ```ruby 173 | # app/graphql/resolvers/post_search.rb 174 | module Resolvers 175 | class PostSearch < Resolvers::BaseSearchResolver 176 | type [Types::PostType], null: false 177 | description "Lists posts" 178 | 179 | scope { Post.all } 180 | 181 | option(:id, type: Int) { |scope, value| scope.where id: value } 182 | option(:title, type: String) { |scope, value| scope.where title: value } 183 | option(:body, type: Int) { |scope, value| scope.where rating: value } 184 | option(:created_at, type: GraphQL::Types::ISO8601DateTime) { |scope, value| scope.where created_at: value } 185 | option(:updated_at, type: GraphQL::Types::ISO8601DateTime) { |scope, value| scope.where updated_at: value } 186 | 187 | def resolve 188 | [] 189 | end 190 | 191 | end 192 | end 193 | ``` 194 | 195 | This will also insert a search field into the beginning of query_type.rb 196 | 197 | ```ruby 198 | #app/graphql/types/query_type.rb 199 | module Types 200 | class QueryType < Types::BaseObject 201 | field :posts, resolver: Resolvers::PostSearch 202 | ... 203 | ``` 204 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | gemspec = eval(File.read("graphql-rails-generators.gemspec")) 2 | 3 | task :build => "#{gemspec.full_name}.gem" 4 | 5 | file "#{gemspec.full_name}.gem" => gemspec.files + ["graphql-rails-generators.gemspec"] do 6 | system "gem build graphql-rails-generators.gemspec" 7 | system "git tag v#{GraphqlRailsGenerators::VERSION}" 8 | end -------------------------------------------------------------------------------- /graphql-rails-generators.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/graphql-rails-generators/version", __FILE__) 2 | Gem::Specification.new do |s| 3 | s.name = 'graphql-rails-generators' 4 | s.version = GraphqlRailsGenerators::VERSION 5 | s.platform = Gem::Platform::RUBY 6 | s.date = '2019-11-26' 7 | s.summary = "Rails graphql generators" 8 | s.description = "Rails graphql generators" 9 | s.authors = ["Alex Sharp"] 10 | s.email = 'ajsharp@gmail.com' 11 | s.files = Dir["{lib}/**/*.rb", "LICENSE", "*.md"] 12 | s.require_path = 'lib' 13 | s.homepage = 'https://github.com/ajsharp/graphql-rails-generators' 14 | s.license = 'MIT' 15 | end -------------------------------------------------------------------------------- /lib/generators/gql/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generate GraphQL types, mutations, and input types from your rails models. 3 | 4 | Example: 5 | rails generate gql:model_type model_type 6 | -------------------------------------------------------------------------------- /lib/generators/gql/gql_generator_base.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | module Gql 4 | module GqlGeneratorBase 5 | protected 6 | 7 | # Generate a namedspaced class name with the mutation prefix 8 | def prefixed_class_name(prefix) 9 | (class_path + ["#{prefix}_#{file_name}"]).map!(&:camelize).join("::") 10 | end 11 | 12 | def type_map 13 | { 14 | integer: 'Int', 15 | string: 'String', 16 | boolean: 'Boolean', 17 | decimal: 'Float', 18 | datetime: 'GraphQL::Types::ISO8601DateTime', 19 | date: 'GraphQL::Types::ISO8601Date', 20 | hstore: 'GraphQL::Types::JSON', 21 | text: 'String', 22 | json: 'GraphQL::Types::JSON', 23 | jsonb: 'GraphQL::Types::JSON' 24 | } 25 | end 26 | 27 | def map_model_types(model_name) 28 | klass = model_name.constantize 29 | associations = klass.reflect_on_all_associations(:belongs_to) 30 | bt_columns = associations.map(&:foreign_key) 31 | 32 | klass.columns 33 | .reject { |col| bt_columns.include?(col.name) } 34 | .reject { |col| type_map[col.type].nil? } 35 | .map do |col| 36 | { 37 | name: col.name, 38 | null: col.null, 39 | gql_type: klass.primary_key == col.name ? 'GraphQL::Types::ID' : type_map[col.type] 40 | } 41 | end 42 | end 43 | 44 | def root_directory(namespace) 45 | "app/graphql/#{namespace.underscore}" 46 | end 47 | 48 | def wrap_in_namespace(namespace) 49 | namespace = namespace.split('::') 50 | namespace.shift if namespace[0].empty? 51 | 52 | code = namespace.each_with_index.map { |name, i| " " * i + "module #{name}" }.join("\n") 53 | code << "\n" << yield(namespace.size) << "\n" 54 | code << (namespace.size - 1).downto(0).map { |i| " " * i + "end" }.join("\n") 55 | code 56 | end 57 | 58 | def class_with_fields(namespace, name, superclass, fields) 59 | wrap_in_namespace(namespace) do |indent| 60 | klass = [] 61 | klass << sprintf("%sclass %s < %s", " " * indent, name, superclass) 62 | 63 | fields.each do |field| 64 | klass << sprintf("%sfield :%s, %s, null: %s", " " * (indent + 1), field[:name], field[:gql_type], field[:null]) 65 | end 66 | 67 | klass << sprintf("%send", " " * indent) 68 | klass.join("\n") 69 | end 70 | end 71 | 72 | def class_with_arguments(namespace, name, superclass, fields) 73 | wrap_in_namespace(namespace) do |indent| 74 | klass = [] 75 | klass << sprintf("%sclass %s < %s", " " * indent, name, superclass) 76 | 77 | fields.each do |field| 78 | klass << sprintf("%sargument :%s, %s, required: %s", " " * (indent + 1), field[:name], field[:gql_type], !field[:null]) 79 | end 80 | 81 | klass << sprintf("%send", " " * indent) 82 | klass.join("\n") 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/generators/gql/input_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gql_generator_base' 2 | module Gql 3 | class InputGenerator < Rails::Generators::Base 4 | include GqlGeneratorBase 5 | source_root File.expand_path('../templates', __FILE__) 6 | 7 | argument :model_name, type: :string 8 | 9 | class_option :name, type: :string 10 | class_option :include_columns, type: :array, default: [] 11 | class_option :superclass, type: :string, default: 'Types::BaseInputObject' 12 | class_option :namespace, type: :string, default: 'Types::Input' 13 | 14 | def generate_input_type 15 | name = options['name'].nil? ? "#{model_name}Input" : options['name'] 16 | superclass = options['superclass'] 17 | 18 | ignore = ['id', 'created_at', 'updated_at'] 19 | fields = map_model_types(model_name) 20 | fields.reject! { |field| ignore.include?(field[:name]) } 21 | if options['include_columns'].any? 22 | fields.reject! { |field| !options['include_columns'].include?(field[:name]) } 23 | end 24 | 25 | code = class_with_arguments(options['namespace'], name, superclass, fields) 26 | file_name = File.join(root_directory(options['namespace']), "#{name.underscore}.rb") 27 | 28 | create_file file_name, code 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/generators/gql/model_search_base_generator.rb: -------------------------------------------------------------------------------- 1 | module Gql 2 | class ModelSearchBaseGenerator < Rails::Generators::Base 3 | source_root File.expand_path('../templates', __FILE__) 4 | def generate_model_search_base 5 | gem 'search_object_graphql' 6 | template('model_search_base.rb', "app/graphql/resolvers/base_search_resolver.rb") 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/generators/gql/model_search_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gql_generator_base' 2 | module Gql 3 | class ModelSearchGenerator < Rails::Generators::Base 4 | include GqlGeneratorBase 5 | source_root File.expand_path('../templates', __FILE__) 6 | argument :model_name, type: :string 7 | 8 | def search 9 | inject_into_file( 10 | "app/graphql/types/query_type.rb", 11 | "\t\tfield :#{model_name.downcase.pluralize}, resolver: Resolvers::#{model_name}Search \n", 12 | :after => "class QueryType < Types::BaseObject\n" 13 | ) 14 | file_name = "#{model_name.underscore}_search" 15 | @fields = map_model_types(model_name) 16 | template('model_search.rb', "app/graphql/resolvers/#{file_name}.rb") 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/generators/gql/model_type_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gql_generator_base' 2 | module Gql 3 | class ModelTypeGenerator < Rails::Generators::Base 4 | include GqlGeneratorBase 5 | source_root File.expand_path('../templates', __FILE__) 6 | 7 | argument :model_name, type: :string 8 | 9 | class_option :name, type: :string 10 | class_option :include_columns, type: :array, default: [] 11 | class_option :superclass, type: :string, default: 'Types::BaseObject' 12 | class_option :namespace, type: :string, default: 'Types' 13 | 14 | def type 15 | name = options['name'].nil? ? "#{model_name}Type" : options['name'] 16 | 17 | superclass = options['superclass'] 18 | 19 | fields = map_model_types(model_name) 20 | if options['include_columns'].any? 21 | fields.reject! { |field| !options['include_columns'].include?(field[:name]) } 22 | end 23 | 24 | code = class_with_fields(options['namespace'], name, superclass, fields) 25 | file_name = File.join(root_directory(options['namespace']), "#{name.underscore}.rb") 26 | 27 | create_file file_name, code 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/generators/gql/mutation_generator.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators/named_base" 2 | require_relative 'gql_generator_base' 3 | 4 | module Gql 5 | class MutationGenerator < Rails::Generators::NamedBase 6 | include GqlGeneratorBase 7 | remove_argument :name # remove name base default arg 8 | 9 | argument :mutation_prefix, type: :string 10 | argument :model_name, type: :string 11 | source_root File.expand_path('../templates', __FILE__) 12 | 13 | # hack to keep NamedBase helpers working 14 | def name 15 | model_name 16 | end 17 | 18 | def mutation 19 | file_name = "#{mutation_prefix}_#{singular_name}" 20 | template('model_mutation.rb', "app/graphql/mutations/#{class_path.join('/')}/#{file_name.underscore}.rb") 21 | insert_into_file("app/graphql/types/mutation_type.rb", after: " class MutationType < Types::BaseObject\n") do 22 | "\t\tfield :#{file_name.camelcase(:lower)}, mutation: Mutations::#{prefixed_class_name(mutation_prefix)}\n" 23 | end 24 | end 25 | end 26 | 27 | end -------------------------------------------------------------------------------- /lib/generators/gql/mutations_generator.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators/named_base" 2 | require_relative 'gql_generator_base' 3 | 4 | module Gql 5 | class MutationsGenerator < Rails::Generators::NamedBase 6 | include GqlGeneratorBase 7 | source_root File.expand_path('../templates', __FILE__) 8 | desc "Generate create, update and delete generators for a model." 9 | 10 | def mutations 11 | insert_into_file("app/graphql/mutations/base_mutation.rb", before: "\tend\nend") do 12 | "def model_errors!(model)\n# define me!\n" 13 | end 14 | generate_mutation('update') 15 | generate_mutation('create') 16 | generate_mutation('delete') 17 | end 18 | 19 | protected 20 | def generate_mutation(prefix) 21 | file_name = "#{prefix}_#{singular_name}" 22 | template("#{prefix}_mutation.rb", "app/graphql/mutations/#{class_path.join('/')}/#{file_name.underscore}.rb") 23 | insert_into_file("app/graphql/types/mutation_type.rb", after: " class MutationType < Types::BaseObject\n") do 24 | "\t\tfield :#{file_name.camelcase(:lower)}, mutation: Mutations::#{prefixed_class_name(prefix)}\n" 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/generators/gql/templates/create_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | class <%= prefixed_class_name('Create') %> < Mutations::BaseMutation 3 | field :<%= singular_name %>, Types::<%= name %>Type, null: true 4 | 5 | argument :attributes, Types::Input::<%= name %>Input, required: true 6 | 7 | def resolve(attributes:) 8 | model = <%= name %>.new(attributes.to_h) 9 | 10 | if model.save 11 | {<%= singular_name %>: model} 12 | else 13 | model_errors!(model) 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/generators/gql/templates/delete_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | class <%= prefixed_class_name('Delete') %> < Mutations::BaseMutation 3 | field :<%= singular_name %>, Types::<%= name %>Type, null: true 4 | 5 | argument :id, GraphQL::Types::ID, required: true 6 | 7 | def resolve(id:) 8 | model = <%= class_name %>.find(id) 9 | 10 | model.destroy 11 | {<%= singular_name %>: model} 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/generators/gql/templates/model_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | class <%= prefixed_class_name(mutation_prefix) %> < Mutations::BaseMutation 3 | field :<%= singular_name %>, Types::<%= @model_name %>Type, null: true 4 | 5 | argument :attributes, Types::Input::<%= @model_name %>Input, required: true 6 | argument :id, GraphQL::Types::ID, required: false 7 | 8 | def resolve(attributes:, id: nil) 9 | model = find_or_build_model(id) 10 | model.attributes = attributes.to_h 11 | 12 | if model.save 13 | {<%= singular_name %>: model} 14 | else 15 | {errors: model.errors.full_messages} 16 | end 17 | end 18 | 19 | def find_or_build_model(id) 20 | if id 21 | <%= @model_name %>.find(id) 22 | else 23 | <%= @model_name %>.new 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/generators/gql/templates/model_search.rb: -------------------------------------------------------------------------------- 1 | module Resolvers 2 | class <%= @resolver_prefix %><%= @model_name %>Search < Resolvers::BaseSearchResolver 3 | type [Types::<%= @model_name %>Type], null: false 4 | description "Lists <%= @model_name.downcase.pluralize %>" 5 | 6 | scope { <%= @model_name %>.all } 7 | 8 | <% @fields.each do |field| -%> 9 | option(:<%= field[:name] %>, type: <%= field[:gql_type] %>) { |scope, value| scope.where <%= field[:name] %>: value } 10 | <% end %> 11 | def resolve 12 | [] 13 | end 14 | 15 | end 16 | end -------------------------------------------------------------------------------- /lib/generators/gql/templates/model_search_base.rb: -------------------------------------------------------------------------------- 1 | module Resolvers 2 | class BaseSearchResolver < GraphQL::Schema::Resolver 3 | require 'search_object' 4 | require 'search_object/plugin/graphql' 5 | include SearchObject.module(:graphql) 6 | end 7 | end -------------------------------------------------------------------------------- /lib/generators/gql/templates/update_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | class <%= prefixed_class_name('Update') %> < Mutations::BaseMutation 3 | field :<%= singular_name %>, Types::<%= name %>Type, null: true 4 | 5 | argument :id, GraphQL::Types::ID, required: true 6 | argument :attributes, Types::Input::<%= name %>Input, required: true 7 | 8 | def resolve(attributes:, id:) 9 | model = <%= class_name %>.find(id) 10 | 11 | if model.update_attributes(attributes.to_h) 12 | {<%= singular_name %>: model} 13 | else 14 | model_errors!(model) 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/graphql-rails-generators/version.rb: -------------------------------------------------------------------------------- 1 | module GraphqlRailsGenerators 2 | VERSION = '1.1.1' 3 | end --------------------------------------------------------------------------------