├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── graphql-sugar.gemspec ├── lib ├── generators │ └── graphql │ │ ├── mutator │ │ ├── USAGE │ │ ├── mutator_generator.rb │ │ └── templates │ │ │ └── mutator.erb │ │ ├── resolver │ │ ├── USAGE │ │ ├── resolver_generator.rb │ │ └── templates │ │ │ └── resolver.erb │ │ └── sugar │ │ ├── USAGE │ │ ├── sugar_generator.rb │ │ └── templates │ │ ├── application_function.erb │ │ ├── application_mutator.erb │ │ └── application_resolver.erb └── graphql │ ├── sugar.rb │ └── sugar │ ├── boot.rb │ ├── define │ ├── attribute.rb │ ├── attributes.rb │ ├── model_class.rb │ ├── mutator.rb │ ├── parameter.rb │ ├── relationship.rb │ ├── relationships.rb │ └── resolver.rb │ ├── function.rb │ ├── mutator.rb │ ├── resolver.rb │ └── version.rb └── spec ├── graphql └── sugar_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.0 5 | before_install: gem install bundler -v 1.14.3 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.6] - 2018-05-18 10 | ### Fixed 11 | - Support for MySQL by conditionally checking PostgreSQL-specific `array?` 12 | 13 | ## [0.1.5] - 2018-02-13 14 | ### Fixed 15 | - Add checks for `allow_blank` and `allow_nil` (#3) 16 | 17 | ## [0.1.4] - 2018-01-26 18 | ### Fixed 19 | - Fix has_many through relationships (#2) 20 | 21 | ## [0.1.3] - 2017-12-12 22 | ### Fixed 23 | - Fix has_one relationship (#1) 24 | 25 | ## [0.1.2] - 2017-11-07 26 | ### Added 27 | - This CHANGELOG file 28 | 29 | ### Fixed 30 | - Allow resolver type to be overridden correctly 31 | 32 | ## [0.1.1] - 2017-10-25 33 | ### Changed 34 | - Improve README 35 | 36 | ## 0.1.0 - 2017-10-25 37 | ### Added 38 | - First release of this gem 39 | 40 | [Unreleased]: https://github.com/keepworks/graphql-sugar/compare/v0.1.6...HEAD 41 | [0.1.6]: https://github.com/keepworks/graphql-sugar/compare/v0.1.5...v0.1.6 42 | [0.1.5]: https://github.com/keepworks/graphql-sugar/compare/v0.1.4...v0.1.5 43 | [0.1.4]: https://github.com/keepworks/graphql-sugar/compare/v0.1.3...v0.1.4 44 | [0.1.3]: https://github.com/keepworks/graphql-sugar/compare/v0.1.2...v0.1.3 45 | [0.1.2]: https://github.com/keepworks/graphql-sugar/compare/v0.1.1...v0.1.2 46 | [0.1.1]: https://github.com/keepworks/graphql-sugar/compare/v0.1.0...v0.1.1 47 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at pradeep@keepworks.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in graphql-sugar.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 KeepWorks Technologies Pvt. Ltd. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL::Sugar 2 | 3 | A sweet, extended DSL written on top of the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) gem. 4 | 5 | **Looking for a quick overview of this gem in action?** Head over to the [Usage](#usage) section. 6 | 7 | This gem allows you to: 8 | 9 | * Easily write [object types](#object-types) and [input types](#input-types) that are backed by ActiveRecord models. 10 | * Automatically convert field names to snake_case. 11 | * Automatically add `id`, `createdAt` and `updatedAt` fields if these columns exist in your database schema. 12 | * Automatically determine the type of the field, based on your database schema and model validation rules, keeping things DRY. 13 | * Easily write [resolvers](#resolvers) and [mutators](#mutators) to encapsulate query and mutation logic. 14 | * Provide an object-oriented layer, allowing easy refactoring of common code across queries and mutations. 15 | * Look like (and function very similar to) Rails controllers, so that writing them is a breeze. 16 | 17 | ## Installation 18 | 19 | ```ruby 20 | gem 'graphql' 21 | gem 'graphql-sugar' 22 | ``` 23 | 24 | And then execute: 25 | 26 | $ bundle 27 | 28 | And finally, do some initial setup: 29 | 30 | $ rails g graphql:sugar 31 | 32 | ## Usage 33 | 34 | This section provides a quick overview of the how simple the DSL can be, as well as a general workflow to follow: 35 | 36 | ### Writing Queries 37 | 38 | Create the ObjectType: 39 | 40 | ```ruby 41 | Types::PostType = GraphQL::ObjectType.define do 42 | model_class Post 43 | 44 | attribute :title 45 | attribute :content 46 | attribute :isPublic 47 | 48 | relationship :user 49 | relationship :comments 50 | end 51 | ``` 52 | 53 | Create a [Resolver](#resolvers): 54 | 55 | ```ruby 56 | class PostResolver < ApplicationResolver 57 | parameter :id, !types.ID 58 | 59 | def resolve 60 | Post.find(params[:id]) 61 | end 62 | end 63 | ``` 64 | 65 | Expose the Resolver: 66 | 67 | ```ruby 68 | Types::QueryType = GraphQL::ObjectType.define do 69 | name 'Query' 70 | 71 | resolver :post 72 | end 73 | ``` 74 | 75 | ### Writing Mutations 76 | 77 | Create the InputObjectType: 78 | 79 | ```ruby 80 | Inputs::PostInputType = GraphQL::InputObjectType.define do 81 | name 'PostInput' 82 | 83 | model_class Post 84 | 85 | parameter :title 86 | parameter :content 87 | end 88 | ``` 89 | 90 | Create a [Mutator](#mutators): 91 | 92 | ```ruby 93 | class CreatePostMutator < ApplicationMutator 94 | parameter :input, !Inputs::PostInputType 95 | 96 | type !Types::PostType 97 | 98 | def mutate 99 | Post.create!(params[:input]) 100 | end 101 | end 102 | ``` 103 | 104 | Expose the Mutator: 105 | 106 | ```ruby 107 | Types::MutationType = GraphQL::ObjectType.define do 108 | name 'Mutation' 109 | 110 | mutator :createPost 111 | end 112 | ``` 113 | 114 | ## Usage 115 | 116 | ### Object Types 117 | 118 | Start by generating an ObjectType as you normally would: 119 | 120 | $ rails g graphql:object Post 121 | 122 | This would create the following under `app/graphql/types/post_type.rb`: 123 | 124 | ```ruby 125 | Types::PostType = GraphQL::ObjectType.define do 126 | name "Post" 127 | end 128 | ``` 129 | 130 | Replace the `name` line with a `model_class` declaration: 131 | 132 | ```ruby 133 | Types::PostType = GraphQL::ObjectType.define do 134 | model_class Post 135 | end 136 | ``` 137 | 138 | This automatically sets the name as `PostType`. If you wish to overwrite the name, you can pass a second argument: 139 | 140 | ```ruby 141 | Types::PostType = GraphQL::ObjectType.define do 142 | model_class Post, 'PostObject' 143 | end 144 | ``` 145 | 146 | The `model_class` declaration is **required** to use rest of the extended ObjectType DSL (like `attributes`, `attribute`, `relationships`, `relationship`, etc). If you forget to declare it however, a helpful exception is raised. :smile: 147 | 148 | #### Defining attributes 149 | 150 | *Normally*, this is how you would add a couple of fields to your ObjectType: 151 | 152 | ```ruby 153 | Types::PostType = GraphQL::ObjectType.define do 154 | model_class Post 155 | 156 | field :id, !types.ID 157 | field :title, !types.String 158 | field :content, types.String 159 | field :isPublic, !types.Boolean, property: :is_public 160 | field :createdAt 161 | field :updatedAt 162 | end 163 | ``` 164 | 165 | However, using GraphQL::Sugar, you can now shorten this to: 166 | 167 | ```ruby 168 | Types::PostType = GraphQL::ObjectType.define do 169 | model_class Post 170 | 171 | attribute :title 172 | attribute :content 173 | attribute :isPublic 174 | end 175 | ``` 176 | 177 | Under the hood: 178 | 179 | * The `id`, `createdAt` and `updatedAt` fields are automatically added if your model has those attributes. 180 | * The type for the rest of the fields are automatically determined based on your `schema.rb` and model validations. (Read more about [automatic type resolution](#automatic-type-resolution).) 181 | * The fields automatically resolve to the snake_cased method names of the attribute name provided (eg. `isPublic` => `is_public`). 182 | 183 | You can shorten this further [active_model_serializers](https://github.com/rails-api/active_model_serializers)-style: 184 | 185 | ```ruby 186 | Types::PostType = GraphQL::ObjectType.define do 187 | model_class Post 188 | 189 | attributes :title, :content, :isPublic 190 | end 191 | ``` 192 | 193 | Or even more simply: 194 | 195 | ```ruby 196 | Types::PostType = GraphQL::ObjectType.define do 197 | model_class Post 198 | 199 | attributes 200 | end 201 | ``` 202 | 203 | ... which automatically includes *all* the attributes of a model based on your schema. While NOT recommended for production, this provides easy scaffolding of model-backed object types during development. 204 | 205 | Internally `attribute` just defines a `field`, but automatically determines the type and resolves to the model's snake_cased attribute. For simplicity, it follows the *exact same syntax* as `field`, so you can override type or specify a `resolve:` function: 206 | 207 | ```ruby 208 | Types::PostType = GraphQL::ObjectType.define do 209 | model_class Post 210 | 211 | attribute :thumbnail, types.String, resolve: ->(obj, args, ctx) { obj.picture_url(:thumb) } 212 | end 213 | ``` 214 | 215 | This is useful (and necessary) if you wish to expose `attr_accessor`s defined in your model. (Read more about [automatic type resolution](#automatic-type-resolution).) 216 | 217 | **Side Note:** You _can_ always mix in good ol' `field`s along with `attribute`s if you really need to access the old DSL: 218 | 219 | ```ruby 220 | Types::PostType = GraphQL::ObjectType.define do 221 | model_class Post 222 | 223 | attribute :title 224 | field :isArchived, types.Boolean, resolve: ->(obj, args, ctx) { obj.is_archived? } 225 | end 226 | ``` 227 | 228 | However, since the syntax is pretty much the same, it is preferable to use either `field` or `attribute` throughout the type definition for the sake of uniformity. You may have a non-model backed ObjectType for example, which can use `field`s. 229 | 230 | #### Defining relationships 231 | 232 | Assume the Post model has the following associations: 233 | 234 | ```ruby 235 | class Post < ApplicationRecord 236 | belongs_to :user 237 | has_many :comments 238 | end 239 | ``` 240 | 241 | *Normally*, this is how you would define the relationship in your ObjectType: 242 | 243 | ```ruby 244 | Types::PostType = GraphQL::ObjectType.define do 245 | model_class Post 246 | 247 | field :userId, !types.ID, property: :user_id 248 | field :user, Types::UserType 249 | 250 | field :comments, !types[Types::CommentType] 251 | end 252 | ``` 253 | 254 | However, using GraphQL::Sugar, you can now shorten this to: 255 | 256 | ```ruby 257 | Types::PostType = GraphQL::ObjectType.define do 258 | model_class Post 259 | 260 | relationship :user 261 | relationship :comments 262 | end 263 | ``` 264 | 265 | Under the hood: 266 | 267 | * If the relationship is **belongs_to**, it automatically defines a field for the corresponding foreign key. It also determines the type and marks the association as non-null using [automatic type resolution](#automatic-type-resolution). 268 | * If the relationship is **has_one** or **has_many**, it first looks for a corresponding [Resolver](#resolvers) (eg. in this case, `CommentsResolver`). If it doesn't find one, it defaults to calling method of the underlying association on the object (eg. `obj.comments`) 269 | 270 | You can shorten the above code to: 271 | 272 | ```ruby 273 | Types::PostType = GraphQL::ObjectType.define do 274 | model_class Post 275 | 276 | relationships :user, :comments 277 | end 278 | ``` 279 | 280 | Or even more simply: 281 | 282 | ```ruby 283 | Types::PostType = GraphQL::ObjectType.define do 284 | model_class Post 285 | 286 | relationships 287 | end 288 | ``` 289 | 290 | ... which automatically reflects on *all* your model associations and includes them. While NOT recommended for production, this provides easy scaffolding of model-backed object types during development. 291 | 292 | **Side Note:** Unlike `attribute`, `relationship` is not just syntactic sugar for `field` and it does much more. It is recommended that you revert to using `field`s (rather than `attribute`) if you need to achieve a specific behavior involving associations. For example: 293 | 294 | ```ruby 295 | Types::PostType = GraphQL::ObjectType.define do 296 | model_class Post 297 | 298 | relationship :user 299 | 300 | field :recentComments, !types[Types::CommentType], resolve: ->(obj, args, ctx) { 301 | obj.comments.not_flagged.recent.limit(3) 302 | } 303 | end 304 | end 305 | ``` 306 | 307 | #### Automatic Type Resolution 308 | 309 | Your model attribute's type is automatically determined using Rails' reflection methods, as follows: 310 | 311 | * First, we look at the column type: 312 | * `:integer` gets mapped to `types.Int` (`GraphQL::INT_TYPE`), 313 | * `:float` and `:decimal` get mapped to `types.Float` (`GraphQL::FLOAT_TYPE`), 314 | * `:boolean` gets mapped to `types.Boolean` (`GraphQL::BOOLEAN_TYPE`), 315 | * and the rest get mapped to `types.String` (`GraphQL::STRING_TYPE`). 316 | * Then, we determine the non-nullability based on whether: 317 | * You have specified `null: false` for the column in your schema, or 318 | * You have specified `presence: true` validation for the attribute in your model. 319 | 320 | In instances where a type cannot be automatically determined, you must provide the type yourself. For example, `attr_accessor`s are not persisted and don't have a corresponding column in your database schema. 321 | 322 | ### Input Types 323 | 324 | *Normally*, this is how you would define your InputObjectType: 325 | 326 | ```ruby 327 | Inputs::PostInputType = GraphQL::InputObjectType.define do 328 | name 'PostInput' 329 | 330 | argument :title, types.String 331 | argument :content, types.String 332 | argument :isPublic, types.Boolean, as: :is_public 333 | end 334 | ``` 335 | 336 | However, using GraphQL::Sugar, you can now shorten this to: 337 | 338 | ```ruby 339 | Inputs::PostInputType = GraphQL::InputObjectType.define do 340 | name 'PostInput' 341 | 342 | model_class 'Post' 343 | 344 | parameter :title 345 | parameter :content 346 | parameter :isPublic 347 | end 348 | ``` 349 | 350 | Under the hood, 351 | * `parameter` uses the same [automatic type resolution](#automatic-type-resolution) as `attribute`, but creates arguments that are not-null by default. The default behavior passes all values to be validated in the model instead, in order to return proper error messages in the response. (**TODO:** Allow this behavior to be configured via an initializer.) 352 | * It allows sets the `:as` value to the snake_cased form of the provided name. (eg. `:isPublic` => `:is_public`). This allows us to easily pass them into ActiveRecord's `create` and `update_attributes` methods. 353 | 354 | You can override the type to make a field non-null as follows: 355 | 356 | ```ruby 357 | Inputs::PostInputType = GraphQL::InputObjectType.define do 358 | name 'PostInput' 359 | 360 | model_class 'Post' 361 | 362 | parameter :title, !types.String 363 | parameter :content 364 | end 365 | ``` 366 | 367 | ### Resolvers 368 | 369 | In its simplest form, a Resolver simply inherits from `ApplicationResolver` and contains a `#resolve` method. 370 | 371 | ```ruby 372 | class PostsResolver < ApplicationResolver 373 | def resolve 374 | Post.all 375 | end 376 | end 377 | ``` 378 | 379 | To expose the resolver as a field, declare it in your root QueryType: 380 | 381 | ```ruby 382 | Types::QueryType = GraphQL::ObjectType.define do 383 | name 'Query' 384 | 385 | resolver :posts 386 | end 387 | ``` 388 | 389 | To declare arguments, you can use the `parameter` keyword which follows the same syntax: 390 | 391 | ```ruby 392 | class PostResolver < ApplicationResolver 393 | parameter :id, !types.ID 394 | 395 | def resolve 396 | Post.find(params[:id]) 397 | end 398 | end 399 | ``` 400 | 401 | The benefit is that all `parameter`s (read: arguments) are loaded into a `params` object, with all keys transformed into snake_case. This allows them to be easily used with ActiveRecord methods like `where` and `find_by`. 402 | 403 | You also have `object` and `context` available in your resolve method: 404 | 405 | ```ruby 406 | class PostsResolver < ApplicationResolver 407 | def resolve 408 | (object || context[:current_user]).posts 409 | end 410 | end 411 | ``` 412 | 413 | #### Thinking in Graphs *using Resolvers* 414 | 415 | Assume the following GraphQL query ("fetch 10 posts, along with the authors and 2 of their highest rated posts."): 416 | 417 | ``` 418 | query { 419 | posts(limit: 10) { 420 | title 421 | content 422 | 423 | user { 424 | name 425 | 426 | posts(limit: 2, sort: "rating_desc") { 427 | title 428 | rating 429 | } 430 | } 431 | } 432 | } 433 | ``` 434 | 435 | When executed, we resolve both the first and second `posts` using `PostsResolver`. This means: 436 | 437 | 1. All the `argument`s (or `parameter`s) available to your top level `posts` are available to all your nested `posts`s through relationships without any extra work. 438 | 439 | 2. The `object` value passed to your `PostsResolver#resolve` function is *very* important. This would be a good place to perform an authorization check to see if the current user has access to this relationship on the `object`. 440 | 441 | **A quick detour:** At the top of your graph, you have your **root_value** ([read more](http://graphql-ruby.org/queries/executing_queries.html#root-value)), which the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) library allows you to set for your schema. By default, this is `null`. You can either *explicitly* set this root_value, or *implicitly* consider to be the current user (or current organization, or whatever your application deems it to be). 442 | 443 | For example, 444 | 445 | ```ruby 446 | class PostsResolver < ApplicationResolver 447 | def resolve 448 | parent_object = (object || context[:current_user]) 449 | authorize! :view_posts, parent_object 450 | 451 | parent_object.posts 452 | end 453 | end 454 | ``` 455 | 456 | ### Mutators 457 | 458 | In its simplest form, a Mutator simply inherits from `ApplicationMutator` and contains a `#mutate` method: 459 | 460 | ```ruby 461 | class CreatePostMutator < ApplicationMutator 462 | parameter :input, !Inputs::PostInputType 463 | 464 | type !Types::PostType 465 | 466 | def mutate 467 | Post.create!(params[:input]) 468 | end 469 | end 470 | ``` 471 | 472 | To expose the mutator as a field, declare it in your root MutationType: 473 | 474 | ```ruby 475 | Types::MutationType = GraphQL::ObjectType.define do 476 | name 'Mutation' 477 | 478 | mutator :createPost 479 | end 480 | ``` 481 | 482 | Just like resolvers, you have access to `object`, `params` and `context`: 483 | 484 | ```ruby 485 | class UpdatePostMutator < ApplicationMutator 486 | parameter :id, !types.ID 487 | parameter :input, !Inputs::PostInputType 488 | 489 | type !Types::PostType 490 | 491 | def mutate 492 | post = context[:current_user].posts.find(params[:id]) 493 | post.update_attributes!(params[:input]) 494 | post 495 | end 496 | end 497 | ``` 498 | 499 | ### Organizing Your Code 500 | 501 | When you install the gem using `rails g graphql:sugar`, it creates the following files: 502 | 503 | ``` 504 | app/graphql/functions/application_function.rb 505 | app/graphql/resolvers/application_resolver.rb 506 | app/graphql/mutators/application_mutator.rb 507 | ``` 508 | 509 | All your resolvers inherit from `ApplicationResolver` and all your mutators inherit from `ApplicationMutator`, both of which in turn inherit from `ApplicationFunction`. You can use these classes to write shared code common to multiple queries, mutations, or both. 510 | 511 | #### Applying OO principles 512 | 513 | *Pagination and Sorting:* You can easily create methods that enable common features. 514 | 515 | ```ruby 516 | class ApplicationResolver < ApplicationFunction 517 | include GraphQL::Sugar::Resolver 518 | 519 | def self.sortable 520 | parameter :sort, types.String 521 | parameter :sortDir, types.String 522 | end 523 | end 524 | ``` 525 | 526 | Use in your other resolvers: 527 | 528 | ```ruby 529 | class PostsResolver < ApplicationResolver 530 | sortable 531 | 532 | def resolve 533 | # ... 534 | end 535 | end 536 | ``` 537 | 538 | *Shared Code:* You can also easily share common code across a specific set of mutators. For example, your `CreatePostMutator` and `UpdatePostMutator` could inherit from `PostMutator`, which inherits from `ApplicationMutator`. 539 | 540 | #### Tips for Large Applications 541 | 542 | In a large app, you can quite easily end up with tons of mutations. During setup, GraphQL::Sugar adds a few lines to your eager_load_paths so you can group them in folders, while maintaining mutations at the root level. For example, 543 | 544 | ``` 545 | # Folder Structure 546 | app/graphql/mutators/ 547 | - posts 548 | - create_post_mutator.rb 549 | - update_post_mutator.rb 550 | - users 551 | - create_user_mutator.rb 552 | - update_user_mutator.rb 553 | - application_mutator.rb 554 | ``` 555 | 556 | ```ruby 557 | Types::MutationType = GraphQL::ObjectType.define do 558 | name 'Mutation' 559 | 560 | mutator :createPost 561 | mutator :updatePost 562 | 563 | mutator :createUser 564 | mutator :updateUser 565 | end 566 | ``` 567 | 568 | ### Generators 569 | 570 | A few basic generators have been written to quickly create some of the boilerplate code. They may not work perfectly, and the generated code may require further editing. 571 | 572 | $ rails g graphql:resolver BlogPosts 573 | 574 | Creates a `BlogPostsResolver` class at `app/graphql/resolvers/blog_posts_resolver.rb`. 575 | 576 | $ rails g graphql:mutator CreateBlogPost 577 | 578 | Creates a `CreateBlogPostMutator` class under `app/graphql/mutators/create_blog_post_mutator.rb`. 579 | 580 | ## Credits 581 | 582 | Many thanks to the work done by the authors of the following gems, which this gem uses as a foundation and/or inspiration: 583 | 584 | - [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) 585 | - [graphql-activerecord](https://github.com/goco-inc/graphql-activerecord) 586 | - [graphql-rails-resolver](https://github.com/colepatrickturner/graphql-rails-resolver) 587 | - [active_model_serializers](https://github.com/rails-api/active_model_serializers) 588 | 589 | --- 590 | 591 | Maintained and sponsored by [KeepWorks](http://www.keepworks.com). 592 | 593 | ![KeepWorks](http://www.keepworks.com/assets/logo-800bbf55fabb3427537cf669dc8cd018.png "KeepWorks") 594 | 595 | ## Contributing 596 | 597 | Bug reports and pull requests are welcome on GitHub at https://github.com/keepworks/graphql-sugar. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 598 | 599 | ## License 600 | 601 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 602 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "graphql/sugar" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /graphql-sugar.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'graphql/sugar/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "graphql-sugar" 8 | spec.version = GraphQL::Sugar::VERSION 9 | spec.authors = ["Pradeep Kumar"] 10 | spec.email = ["pradeep@keepworks.com"] 11 | 12 | spec.summary = "A sweet, extended DSL written on top of the graphql-ruby gem." 13 | spec.homepage = "https://github.com/keepworks/graphql-sugar" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 17 | f.match(%r{^(test|spec|features)/}) 18 | end 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_development_dependency "bundler", "~> 1.14" 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec", "~> 3.0" 26 | end 27 | -------------------------------------------------------------------------------- /lib/generators/graphql/mutator/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a GraphQL::Sugar Mutator. 3 | 4 | Example: 5 | rails generate graphql:mutator Thing 6 | 7 | This will create: 8 | app/graphql/mutators/thing_mutator.rb 9 | -------------------------------------------------------------------------------- /lib/generators/graphql/mutator/mutator_generator.rb: -------------------------------------------------------------------------------- 1 | module Graphql 2 | class MutatorGenerator < Rails::Generators::NamedBase 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def create_mutator 6 | template 'mutator.erb', File.join('app/graphql/mutators', class_path, "#{file_name}_mutator.rb") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/graphql/mutator/templates/mutator.erb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Mutator < ApplicationMutator 2 | <%= class_name %>InputType = GraphQL::InputObjectType.define do 3 | name '<%= class_name %>Input' 4 | 5 | parameter :id, !types.ID 6 | end 7 | 8 | parameter :input, !<%= class_name %>InputType 9 | 10 | <%- type_name = file_name.split('_').drop(1).join('_').camelize -%> 11 | type !Types::<%= type_name %>Type 12 | 13 | def mutate 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/graphql/resolver/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a GraphQL::Sugar Resolver. 3 | 4 | Example: 5 | rails generate graphql:resolver Thing 6 | 7 | This will create: 8 | app/graphql/resolvers/thing_resolver.rb 9 | -------------------------------------------------------------------------------- /lib/generators/graphql/resolver/resolver_generator.rb: -------------------------------------------------------------------------------- 1 | module Graphql 2 | class ResolverGenerator < Rails::Generators::NamedBase 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def create_resolver 6 | template 'resolver.erb', File.join('app/graphql/resolvers', class_path, "#{file_name}_resolver.rb") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/graphql/resolver/templates/resolver.erb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Resolver < ApplicationResolver 2 | def resolve 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/graphql/sugar/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Installs GraphQL::Sugar 3 | 4 | Example: 5 | rails generate graphql:sugar 6 | 7 | This will create: 8 | app/graphql/functions/application_function.rb 9 | app/graphql/resolvers/application_resolver.rb 10 | app/graphql/mutators/application_mutator.rb 11 | -------------------------------------------------------------------------------- /lib/generators/graphql/sugar/sugar_generator.rb: -------------------------------------------------------------------------------- 1 | module Graphql 2 | class SugarGenerator < Rails::Generators::Base 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def add_paths 6 | application { 'config.eager_load_paths += Dir["#{config.root}/app/graphql/functions/**/"]' } 7 | application { 'config.eager_load_paths += Dir["#{config.root}/app/graphql/mutators/**/"]' } 8 | application { 'config.eager_load_paths += Dir["#{config.root}/app/graphql/resolvers/**/"]' } 9 | end 10 | 11 | def create_application_files 12 | template 'application_function.erb', 'app/graphql/functions/application_function.rb' 13 | template 'application_resolver.erb', 'app/graphql/resolvers/application_resolver.rb' 14 | template 'application_mutator.erb', 'app/graphql/mutators/application_mutator.rb' 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/graphql/sugar/templates/application_function.erb: -------------------------------------------------------------------------------- 1 | class ApplicationFunction < GraphQL::Function 2 | include GraphQL::Sugar::Function 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/graphql/sugar/templates/application_mutator.erb: -------------------------------------------------------------------------------- 1 | class ApplicationMutator < ApplicationFunction 2 | include GraphQL::Sugar::Mutator 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/graphql/sugar/templates/application_resolver.erb: -------------------------------------------------------------------------------- 1 | class ApplicationResolver < ApplicationFunction 2 | include GraphQL::Sugar::Resolver 3 | end 4 | -------------------------------------------------------------------------------- /lib/graphql/sugar.rb: -------------------------------------------------------------------------------- 1 | require 'graphql/sugar/version' 2 | 3 | module GraphQL 4 | module Sugar 5 | GRAPHQL_TYPE_MAPPING = { 6 | integer: GraphQL::INT_TYPE, 7 | float: GraphQL::FLOAT_TYPE, 8 | decimal: GraphQL::FLOAT_TYPE, 9 | boolean: GraphQL::BOOLEAN_TYPE, 10 | string: GraphQL::STRING_TYPE 11 | }.freeze 12 | 13 | def self.get_resolver_graphql_type(field_name) 14 | "Types::#{field_name.to_s.classify}Type".constantize 15 | end 16 | 17 | def self.get_resolver_function(field_name) 18 | "#{field_name.to_s.camelize}Resolver".constantize 19 | end 20 | 21 | def self.get_resolver_plural(field_name) 22 | field_string = field_name.to_s 23 | field_string.pluralize == field_string 24 | end 25 | 26 | def self.get_mutator_function(field_name) 27 | "#{field_name.to_s.camelize}Mutator".constantize 28 | end 29 | 30 | def self.get_model_class(type_defn) 31 | model_class = type_defn.metadata[:model_class] 32 | raise "You must define a `model_class` first in `#{type_defn.class}`." if model_class.blank? 33 | model_class 34 | end 35 | 36 | def self.get_column_name(field_name) 37 | field_name.to_s.underscore 38 | end 39 | 40 | def self.get_column_details(model_class, column_name) 41 | column_details = model_class.columns_hash[column_name] 42 | raise "The attribute '#{column_name}' doesn't exist in model '#{model_class}'." if column_details.blank? 43 | column_details 44 | end 45 | 46 | def self.get_graphql_type(model_class, column_name, enforce_non_null: true) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 47 | return GraphQL::ID_TYPE.to_non_null_type if column_name == model_class.primary_key 48 | 49 | column_details = get_column_details(model_class, column_name) 50 | 51 | belongs_to_association = model_class.reflect_on_all_associations(:belongs_to).find { |a| a.foreign_key == column_name } 52 | 53 | type = if model_class.defined_enums.key?(column_name) 54 | GraphQL::STRING_TYPE 55 | elsif belongs_to_association.present? 56 | GraphQL::ID_TYPE 57 | else 58 | GRAPHQL_TYPE_MAPPING[column_details.type] || GraphQL::STRING_TYPE 59 | end 60 | 61 | type = type.to_list_type if column_details.respond_to?(:array?) && column_details.array? 62 | 63 | if enforce_non_null 64 | is_not_null = !column_details.null 65 | is_not_null ||= Sugar.validates_presence?(model_class, column_name) 66 | is_not_null ||= Sugar.validates_presence?(model_class, belongs_to_association.name) if belongs_to_association.present? 67 | type = type.to_non_null_type if is_not_null 68 | end 69 | 70 | type 71 | end 72 | 73 | def self.get_association_name(field_name) 74 | field_name.to_s.underscore 75 | end 76 | 77 | def self.validates_presence?(model_class, column_name) 78 | column_validators = model_class.validators_on(column_name) 79 | column_validators.any? do |validator| 80 | validator.class == ActiveRecord::Validations::PresenceValidator && 81 | !validator.options[:allow_nil] && 82 | !validator.options[:allow_blank] && 83 | !validator.options.key?(:if) && 84 | !validator.options.key?(:unless) 85 | end 86 | end 87 | end 88 | end 89 | 90 | require 'graphql/sugar/define/resolver' 91 | require 'graphql/sugar/define/mutator' 92 | require 'graphql/sugar/define/model_class' 93 | require 'graphql/sugar/define/attribute' 94 | require 'graphql/sugar/define/attributes' 95 | require 'graphql/sugar/define/relationship' 96 | require 'graphql/sugar/define/relationships' 97 | require 'graphql/sugar/define/parameter' 98 | require 'graphql/sugar/function' 99 | require 'graphql/sugar/resolver' 100 | require 'graphql/sugar/mutator' 101 | require 'graphql/sugar/boot' 102 | -------------------------------------------------------------------------------- /lib/graphql/sugar/boot.rb: -------------------------------------------------------------------------------- 1 | GraphQL::ObjectType.accepts_definitions( 2 | resolver: GraphQL::Sugar::Define::Resolver, 3 | mutator: GraphQL::Sugar::Define::Mutator, 4 | model_class: GraphQL::Sugar::Define::ModelClass, 5 | attribute: GraphQL::Sugar::Define::Attribute, 6 | attributes: GraphQL::Sugar::Define::Attributes, 7 | relationship: GraphQL::Sugar::Define::Relationship, 8 | relationships: GraphQL::Sugar::Define::Relationships 9 | ) 10 | 11 | GraphQL::Field.accepts_definitions( 12 | parameter: GraphQL::Sugar::Define::Parameter 13 | ) 14 | 15 | GraphQL::InputObjectType.accepts_definitions( 16 | model_class: GraphQL::Define.assign_metadata_key(:model_class), 17 | parameter: GraphQL::Sugar::Define::Parameter 18 | ) 19 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/attribute.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Attribute 5 | def self.call(type_defn, field_name, type_or_field = nil, desc = nil, **kwargs, &block) # rubocop:disable Metrics/ParameterLists 6 | model_class = Sugar.get_model_class(type_defn) 7 | column_name = Sugar.get_column_name(field_name) 8 | 9 | type_or_field ||= kwargs[:type] if !kwargs[:type].nil? 10 | type_or_field ||= Sugar.get_graphql_type(model_class, column_name) 11 | 12 | kwargs[:property] ||= column_name.to_sym if kwargs[:resolve].nil? 13 | 14 | GraphQL::Define::AssignObjectField.call(type_defn, field_name, type_or_field, desc, **kwargs, &block) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/attributes.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Attributes 5 | def self.call(type_defn, *field_names) 6 | model_class = Sugar.get_model_class(type_defn) 7 | 8 | field_names = model_class.columns_hash.keys.map(&:to_sym) if field_names.count == 0 9 | field_names.each do |field_name| 10 | Sugar::Define::Attribute.call(type_defn, field_name) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/model_class.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module ModelClass 5 | def self.call(type_defn, model_class, type_name = nil) 6 | type_defn.name = type_name || model_class.to_s 7 | type_defn.metadata[:model_class] = model_class 8 | 9 | common_field_names = [:id, :createdAt, :updatedAt] 10 | common_field_names.each do |common_field_name| 11 | begin 12 | Sugar::Define::Attribute.call(type_defn, common_field_name) 13 | rescue => e 14 | Rails.logger.warn e 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/mutator.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Mutator 5 | def self.call(type_defn, field_name, type_or_field = nil, desc = nil, **kwargs, &block) # rubocop:disable Metrics/ParameterLists 6 | # Automatically determine function 7 | function_class = Sugar.get_mutator_function(field_name) 8 | kwargs[:function] ||= function_class.new 9 | kwargs[:resolve] ||= ->(obj, args, ctx) { function_class.new.call(obj, args, ctx) } 10 | 11 | GraphQL::Define::AssignObjectField.call(type_defn, field_name, type_or_field, desc, **kwargs, &block) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/parameter.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Parameter 5 | def self.call(type_defn, name, type = nil, *args, **kwargs, &block) # rubocop:disable Metrics/ParameterLists 6 | model_class = type_defn.metadata[:model_class] 7 | 8 | type ||= kwargs[:type] 9 | 10 | if type.nil? 11 | column_name = Sugar.get_column_name(name) 12 | type = Sugar.get_graphql_type(model_class, column_name, enforce_non_null: false) 13 | end 14 | 15 | if kwargs[:as].nil? 16 | field_name = name.to_s.underscore.to_sym 17 | field_name = "#{field_name}_attributes".to_sym if model_class && model_class.nested_attributes_options[field_name] 18 | kwargs[:as] = field_name 19 | end 20 | 21 | GraphQL::Define::AssignArgument.call(type_defn, name, type, *args, **kwargs, &block) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/relationship.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Relationship 5 | def self.call(type_defn, field_name) 6 | model_class = Sugar.get_model_class(type_defn) 7 | association_name = Sugar.get_association_name(field_name) 8 | association = model_class.reflect_on_association(association_name) 9 | 10 | if association.association_class == ActiveRecord::Associations::BelongsToAssociation 11 | define_belongs_to(type_defn, field_name, model_class, association_name, association) 12 | elsif association.association_class == ActiveRecord::Associations::HasOneAssociation || 13 | association.association_class == ActiveRecord::Associations::HasManyAssociation || 14 | association.association_class == ActiveRecord::Associations::HasManyThroughAssociation 15 | define_has_one_or_many(type_defn, field_name, model_class, association_name, association) 16 | end 17 | end 18 | 19 | def self.define_belongs_to(type_defn, field_name, model_class, association_name, association) 20 | key_field_name = association.foreign_key.to_s.camelize(:lower).to_sym 21 | key_type = GraphQL::ID_TYPE 22 | key_property = association.foreign_key.to_sym 23 | 24 | type = "Types::#{association.klass}Type".constantize 25 | property = association_name.to_sym 26 | 27 | key_column_details = Sugar.get_column_details(model_class, association.foreign_key) 28 | is_not_null = !key_column_details.null || Sugar.validates_presence?(model_class, association_name) 29 | 30 | if is_not_null 31 | key_type = key_type.to_non_null_type 32 | type = type.to_non_null_type 33 | end 34 | 35 | GraphQL::Define::AssignObjectField.call(type_defn, key_field_name, type: key_type, property: key_property) 36 | GraphQL::Define::AssignObjectField.call(type_defn, field_name, type: type, property: property) 37 | end 38 | 39 | def self.define_has_one_or_many(type_defn, field_name, _model_class, association_name, association) 40 | kwargs = {} 41 | 42 | kwargs[:type] = "Types::#{association.klass}Type".constantize 43 | 44 | if association.association_class == ActiveRecord::Associations::HasManyAssociation || 45 | association.association_class == ActiveRecord::Associations::HasManyThroughAssociation 46 | kwargs[:type] = kwargs[:type].to_non_null_type.to_list_type 47 | end 48 | 49 | begin 50 | function_class = Sugar.get_resolver_function(field_name) 51 | kwargs[:function] ||= function_class.new 52 | kwargs[:resolve] ||= ->(obj, args, ctx) { function_class.new.call(obj, args, ctx) } 53 | rescue 54 | kwargs[:property] = association_name.to_sym 55 | end 56 | 57 | GraphQL::Define::AssignObjectField.call(type_defn, field_name, **kwargs) 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/relationships.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Relationships 5 | def self.call(type_defn, *field_names) 6 | model_class = Sugar.get_model_class(type_defn) 7 | 8 | if field_names.count == 0 9 | [:belongs_to, :has_one, :has_many].each do |macro| 10 | model_class.reflect_on_all_associations(macro).each do |association| 11 | field_names << association.name 12 | end 13 | end 14 | end 15 | 16 | field_names.each do |field_name| 17 | Sugar::Define::Relationship.call(type_defn, field_name) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/graphql/sugar/define/resolver.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Define 4 | module Resolver 5 | def self.call(type_defn, field_name, type_or_field = nil, desc = nil, **kwargs, &block) # rubocop:disable Metrics/ParameterLists 6 | type_or_field ||= kwargs[:type] if !kwargs[:type].nil? 7 | 8 | if type_or_field.nil? 9 | # Automatically determine type 10 | type_or_field ||= Sugar.get_resolver_graphql_type(field_name) 11 | 12 | # Automatically determine if plural, modify type to !types[Type] if true 13 | plural = kwargs[:plural] 14 | plural = Sugar.get_resolver_plural(field_name) if plural.nil? 15 | type_or_field = type_or_field.to_list_type.to_non_null_type if plural 16 | end 17 | 18 | # Automatically determine function 19 | function_class = Sugar.get_resolver_function(field_name) 20 | kwargs[:function] ||= function_class.new 21 | kwargs[:resolve] ||= ->(obj, args, ctx) { function_class.new.call(obj, args, ctx) } 22 | 23 | GraphQL::Define::AssignObjectField.call(type_defn, field_name, type_or_field, desc, **kwargs, &block) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/graphql/sugar/function.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Function 4 | def self.included(base) 5 | base.extend ClassMethods 6 | base.class_eval do 7 | attr_reader :object 8 | attr_reader :params 9 | attr_reader :context 10 | end 11 | end 12 | 13 | module ClassMethods 14 | # Workaround: 15 | # A `GraphQL::Function` is supposed to be a 'reusable container for field logic'. 16 | # However, extended Field DSL (specified using `GraphQL::Field.accepts_definitions(...)`) 17 | # is not available within Functions. Therefore, re-defining it here. 18 | def parameter(name, *args, **kwargs, &block) 19 | kwargs[:as] ||= name.to_s.underscore.to_sym 20 | argument(name, *args, **kwargs, &block) 21 | end 22 | end 23 | 24 | def call(obj, args, ctx) 25 | @object = obj 26 | @params = args.to_h.deep_symbolize_keys 27 | @context = ctx 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/graphql/sugar/mutator.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Mutator 4 | def call(obj, args, ctx) 5 | super 6 | 7 | mutate 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/graphql/sugar/resolver.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | module Resolver 4 | def call(obj, args, ctx) 5 | super 6 | 7 | resolve 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/graphql/sugar/version.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Sugar 3 | VERSION = '0.1.6' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/graphql/sugar_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe GraphQL::Sugar do 4 | it "has a version number" do 5 | expect(GraphQL::Sugar::VERSION).not_to be nil 6 | end 7 | 8 | it "does something useful" do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "graphql/sugar" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | config.expect_with :rspec do |c| 9 | c.syntax = :expect 10 | end 11 | end 12 | --------------------------------------------------------------------------------