graphql-sugar 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +601 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/graphql-sugar.gemspec +26 -0
- data/lib/generators/graphql/mutator/USAGE +8 -0
- data/lib/generators/graphql/mutator/mutator_generator.rb +9 -0
- data/lib/generators/graphql/mutator/templates/mutator.erb +15 -0
- data/lib/generators/graphql/resolver/USAGE +8 -0
- data/lib/generators/graphql/resolver/resolver_generator.rb +9 -0
- data/lib/generators/graphql/resolver/templates/resolver.erb +4 -0
- data/lib/generators/graphql/sugar/USAGE +10 -0
- data/lib/generators/graphql/sugar/sugar_generator.rb +17 -0
- data/lib/generators/graphql/sugar/templates/application_function.erb +3 -0
- data/lib/generators/graphql/sugar/templates/application_mutator.erb +3 -0
- data/lib/generators/graphql/sugar/templates/application_resolver.erb +3 -0
- data/lib/graphql/sugar/boot.rb +18 -0
- data/lib/graphql/sugar/define/attribute.rb +19 -0
- data/lib/graphql/sugar/define/attributes.rb +16 -0
- data/lib/graphql/sugar/define/model_class.rb +21 -0
- data/lib/graphql/sugar/define/mutator.rb +16 -0
- data/lib/graphql/sugar/define/parameter.rb +26 -0
- data/lib/graphql/sugar/define/relationship.rb +57 -0
- data/lib/graphql/sugar/define/relationships.rb +23 -0
- data/lib/graphql/sugar/define/resolver.rb +25 -0
- data/lib/graphql/sugar/function.rb +31 -0
- data/lib/graphql/sugar/mutator.rb +11 -0
- data/lib/graphql/sugar/resolver.rb +11 -0
- data/lib/graphql/sugar/version.rb +5 -0
- data/lib/graphql/sugar.rb +99 -0
- metadata +122 -0
data/README.md
ADDED
@@ -0,0 +1,601 @@
|
|
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
|
+

|
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).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
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__)
|
data/bin/setup
ADDED
@@ -0,0 +1,26 @@
|
|
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
|
@@ -0,0 +1,9 @@
|
|
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
|
@@ -0,0 +1,15 @@
|
|
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
|