graphql-activerecord 0.8.0.pre.alpha1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +300 -8
- data/graphql-activerecord.gemspec +4 -4
- data/lib/graphql/models/mutator.rb +1 -1
- data/lib/graphql/models/version.rb +1 -1
- metadata +25 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cc30cdefc71e9c7880e6f49ed3a4817f69ab3ca
|
4
|
+
data.tar.gz: 25d052ad19f4b048ab4498afadf0c4472b1ec974
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5722b4fa3cd5a4dc07b868569e37213ba0c8552b5a17057c7859423c2ca8c9ad846796b6efa04f4bf98ec616d21f3bde880234a3655fd6b4f68ba0dd63b698f
|
7
|
+
data.tar.gz: 3c9ef697f0f9f16db19b80eb9bcdfeda3ad71bb46371cb497875b48a3bbdc8da159c6ef15f58e5bf612d7684d68ceea5dd58dbd7c23c7d8eb05fcbd7c091dfd1
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,10 +1,104 @@
|
|
1
1
|
# GraphQL::Models
|
2
2
|
|
3
|
-
|
3
|
+
This gem is designed to help you map Active Record models to GraphQL types, both for queries and mutations, using the [`graphql`](https://github.com/rmosolgo/graphql-ruby) gem. It assumes that you're using Rails, Relay and PostgreSQL.
|
4
|
+
|
5
|
+
It extends the `define` methods for GraphQL object types to provide a simple syntax for adding fields that access attributes
|
6
|
+
on your model. It also makes it easy to "flatten" models together across associations, or to create fields that just access
|
7
|
+
the association as a separate object type.
|
8
|
+
|
9
|
+
In the process, this gem also converts `snake_case` attribute names into `camelCase` field names. When you go to build a mutation
|
10
|
+
using this gem, it knows how to revert that process, even in cases where the conversion isn't symmetric.
|
11
|
+
|
12
|
+
Here's an example:
|
13
|
+
```ruby
|
14
|
+
EmployeeGraph = GraphQL::ObjectType.define do
|
15
|
+
name "Employee"
|
16
|
+
|
17
|
+
# Looks for an Active Record model called "Employee"
|
18
|
+
backed_by_model :employee do
|
19
|
+
|
20
|
+
# The gem will look at the data type for the attributes, and map them to GraphQL types
|
21
|
+
attr :title
|
22
|
+
attr :salary
|
23
|
+
|
24
|
+
# You can flatten fields across associations to simplify the schema. In this example, an
|
25
|
+
# Employee belongs to a Person.
|
26
|
+
proxy_to :person do
|
27
|
+
# These fields get converted to camel case (ie, firstName, lastName) on the schema
|
28
|
+
attr :first_name
|
29
|
+
attr :last_name
|
30
|
+
attr :email
|
31
|
+
|
32
|
+
# You can also provide the association itself as an object field. In this example, a
|
33
|
+
# Person has one Address. The gem assumes that the corresponding GraphQL object type
|
34
|
+
# is called "AddressGraph".
|
35
|
+
has_one :address
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
You can also build a corresponding mutation, using a very similar syntax. Mutations are more complicated, since they involve
|
42
|
+
not just changing the data, but also validating it. Here's an example:
|
43
|
+
```ruby
|
44
|
+
UpdateEmployeeMutation = GraphQL::Relay::Mutation.define do
|
45
|
+
name "UpdateEmployee"
|
46
|
+
|
47
|
+
input_field :id, !types.ID
|
48
|
+
|
49
|
+
# For mutations, you create a mutator definition. This will add the input fields to your
|
50
|
+
# mutation, and also return an object that you'll use in the resolver to perform the mutation.
|
51
|
+
# The parameters you pass are explained below.
|
52
|
+
mutator_definition = GraphQL::Models.define_mutator(self, Employee, null_behavior: :leave_unchanged) do
|
53
|
+
attr :title
|
54
|
+
attr :salary
|
55
|
+
|
56
|
+
proxy_to :person do
|
57
|
+
attr :first_name
|
58
|
+
attr :last_name
|
59
|
+
attr :email
|
60
|
+
|
61
|
+
# You can use nested input object types to allow making changes across associations with a single mutation.
|
62
|
+
# Unlike querying, you need to be explicit about what fields on associated objects can be changed.
|
63
|
+
nested :address, null_behavior: :set_null do
|
64
|
+
attr :line_1
|
65
|
+
attr :line_2
|
66
|
+
attr :city
|
67
|
+
attr :state
|
68
|
+
attr :postal_code
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return_field :employee, EmployeeGraph
|
74
|
+
|
75
|
+
resolve -> (inputs, context) {
|
76
|
+
# Fetch (or create) the model that the mutation should change
|
77
|
+
model = Employee.find(inputs[:id])
|
78
|
+
|
79
|
+
# Get the mutator
|
80
|
+
mutator = mutator_definition.mutator(model, inputs, context)
|
81
|
+
|
82
|
+
# Call `apply_changes` to update the models. This does not save the changes to the database yet:
|
83
|
+
mutator.apply_changes
|
84
|
+
|
85
|
+
# Let's validate the changes. This will raise an exception that can be caught in middleware:
|
86
|
+
mutator.validate!
|
87
|
+
|
88
|
+
# Verify that the user is allowed to make the changes. Explained below:
|
89
|
+
mutator.authorize!
|
90
|
+
|
91
|
+
# If that passes, let's save the changes and return the result
|
92
|
+
mutator.save!
|
93
|
+
|
94
|
+
{ employee: model }
|
95
|
+
}
|
96
|
+
end
|
97
|
+
```
|
4
98
|
|
5
99
|
## Installation
|
6
100
|
|
7
|
-
|
101
|
+
To get started, you should add this line to your application's Gemfile:
|
8
102
|
|
9
103
|
```ruby
|
10
104
|
gem 'graphql-activerecord'
|
@@ -12,25 +106,223 @@ gem 'graphql-activerecord'
|
|
12
106
|
|
13
107
|
And then execute:
|
14
108
|
|
15
|
-
$ bundle
|
109
|
+
$ bundle install
|
110
|
+
|
111
|
+
Next, you need to supply a few callbacks. I put these inside of `config/initializers` in my Rails app:
|
112
|
+
```ruby
|
113
|
+
# This proc takes a Relay global ID, and returns the Active Record model. It can be the same as
|
114
|
+
# the `object_to_id` proc that you use for global node identification:
|
115
|
+
GraphQL::Models.model_from_id = -> (id, context) {
|
116
|
+
model_type, model_id = NodeHelpers.decode_id(id)
|
117
|
+
model_type.find(model_id)
|
118
|
+
}
|
119
|
+
|
120
|
+
# This proc essentially reverses that process:
|
121
|
+
GraphQL::Models.id_for_model = -> (model_type_name, model_id) {
|
122
|
+
NodeHelpers.encode_id(model_type_name, model_id)
|
123
|
+
}
|
124
|
+
|
125
|
+
# This proc is used when you're authorizing changes to a model inside of a mutator:
|
126
|
+
GraphQL::Models.authorize = -> (context, action, model) {
|
127
|
+
# Action will be either :create, :update, or :destroy
|
128
|
+
# Raise an exception if the action should not proceed
|
129
|
+
user = context['user']
|
130
|
+
model.authorize_changes!(action, user)
|
131
|
+
}
|
132
|
+
```
|
133
|
+
|
134
|
+
Finally, you need to set a few options on your schema:
|
135
|
+
```ruby
|
136
|
+
Schema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy
|
137
|
+
Schema.mutation_execution_strategy = GraphQL::Batch::MutationExecutionStrategy
|
138
|
+
Schema.middleware << GraphQL::Models::Middleware.new
|
139
|
+
```
|
140
|
+
|
141
|
+
## Other notes
|
142
|
+
|
143
|
+
The code in this gem was originally part of our main codebase, before we extracted it. So there are a few places where some of
|
144
|
+
of our own conventions leak through. Eventually, I'd like to clean these up, but you'll have to live with them for now.
|
16
145
|
|
17
|
-
|
146
|
+
### Database compatibility
|
18
147
|
|
19
|
-
|
148
|
+
This gem uses `graphql-batch` to optimize loading associated models in your graph, so that you don't end up with lots of N+1
|
149
|
+
queries. It tries to do that in a way that preserves things like scopes that change the order or filter the rows retrieved.
|
150
|
+
|
151
|
+
Unfortunately, that means that it needs to build some custom SQL expressions, and they might not be compatible with every
|
152
|
+
database engine. They should work correctly on PostgreSQL.
|
153
|
+
|
154
|
+
### Naming of GraphQL types
|
155
|
+
|
156
|
+
If your model is named `Something`, this gem assumes that the corresponding object type is called `SomethingGraph`, and it uses
|
157
|
+
the Rails `String#constantize` method to find it. This is mainly needed when you're specifying associations on your object types.
|
158
|
+
|
159
|
+
### Global ID's
|
160
|
+
|
161
|
+
When you use the `has_one` or `has_many_array` helpers to output associations, the gem will also include a field that only
|
162
|
+
returns the global ID's of the models. To do that, it calls a method named `gid` on the model. You'll need to provide that method
|
163
|
+
for those fields to work. We do that by defining it in a concern that we include into `ActiveRecord::Base`:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
module ActiveRecordExtensions
|
167
|
+
extend ActiveSupport::Concern
|
168
|
+
|
169
|
+
def gid
|
170
|
+
NodeHelpers.encode_id(self.class.name, self.id)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
ActiveRecord::Base.send(:include, ActiveRecordExtensions)
|
175
|
+
```
|
20
176
|
|
21
177
|
## Usage
|
22
178
|
|
23
|
-
|
179
|
+
### GraphQL Enum's
|
180
|
+
|
181
|
+
Active Record allows you to define enum fields on your models. They're stored as integers, but treated as strings in your app.
|
182
|
+
You can use a helper to automatically build GraphQL enum types for them:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
class MyModel < ActiveRecord::Base
|
186
|
+
enum status: [:active, :inactive]
|
187
|
+
graphql_enum :status
|
188
|
+
end
|
189
|
+
|
190
|
+
# You can access the auto-built type if you need to:
|
191
|
+
MyModel.graphql_enum_types[:status]
|
192
|
+
|
193
|
+
# When you use it inside of your GraphQL schema, it'll know to use the GraphQL enum type:
|
194
|
+
MyModelGraph = GraphQL::ObjectType.define do
|
195
|
+
backed_by_model :my_model do
|
196
|
+
attr :status
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
You can also manually specify the type to use, if you just want the type mapping behavior:
|
202
|
+
```ruby
|
203
|
+
graphql_enum :status, type: StatusEnum
|
204
|
+
```
|
205
|
+
|
206
|
+
### Association helpers
|
207
|
+
There are three helpers that you can use to build fields for associated models:
|
208
|
+
|
209
|
+
- `has_one` can be used for either `belongs_to` or `has_one` associations
|
210
|
+
- `has_many_array` will return all of the associated models as a GraphQL list
|
211
|
+
- `has_many_connection` will return a paged connection of the associated models
|
212
|
+
|
213
|
+
### Fields inside of `proxy_to` blocks
|
214
|
+
You can also define ordinary fields inside of `proxy_to` blocks. When you do that, your field will receive the associated model
|
215
|
+
as the object, instead of the original model. This is meant to allow you to take advantage of the optimized association loading
|
216
|
+
that the gem provides:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
backed_by_model :employee do
|
220
|
+
proxy_to :person do
|
221
|
+
field :someCustomField, types.Int do
|
222
|
+
resolve -> (model, args, context) {
|
223
|
+
# model is an instance of Person, not Employee
|
224
|
+
}
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
### Defining Mutations
|
231
|
+
|
232
|
+
When you define a mutation, there are a few parameters that you need to pass. Here's an example:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
mutator_definition = GraphQL::Models.define_mutator(self, Employee, null_behavior: :leave_unchanged)
|
236
|
+
```
|
237
|
+
|
238
|
+
The parameters are:
|
239
|
+
- The definer object: it needs this so that it can create the input fields. You should always pass `self` for this parameter.
|
240
|
+
- The model class that the mutator is changing: it needs this so that it can map attributes to the correct input types.
|
241
|
+
- `null_behavior`: this lets you choose how null values are treated. It's explained below.
|
242
|
+
|
243
|
+
#### Virtual Attributes
|
244
|
+
In your mutator, you can specify virtual attributes on your model, you just need to provide the type:
|
245
|
+
```ruby
|
246
|
+
attr :some_fake_attribute, type: types.String
|
247
|
+
```
|
248
|
+
|
249
|
+
#### null_behavior
|
250
|
+
|
251
|
+
When you build a mutation, you have two options that control how null values are treated. They are meant to allow you
|
252
|
+
to choose behavior similar to HTTP PATCH or HTTP POST, where you may want to update just part of a model without having to supply
|
253
|
+
values for every field.
|
254
|
+
|
255
|
+
You specify which option you'd like using the `null_behavior` parameter when defining the mutation:
|
256
|
+
- `leave_unchanged` means that if the input field contains a null value, it is ignored
|
257
|
+
- `set_null` means that if the input field contains a null value, the attribute will actually be set to `nil`
|
258
|
+
|
259
|
+
##### set_null
|
260
|
+
The `set_null` option is the simpler of the two, and you should probably default to using that option. When you pick this option,
|
261
|
+
null values behave as you would expect: they cause the attribute to be set to `nil`.
|
262
|
+
|
263
|
+
But another important side-effect is that the input fields on the mutation will be marked as non-nullable if the underlying
|
264
|
+
database column is not nullable, or if there is an unconditional presence validator on that field.
|
265
|
+
|
266
|
+
##### leave_unchanged
|
267
|
+
If you select this option, any input fields that contain null values will be ignored. Instead, if you really do want to set a
|
268
|
+
field to null, the gem adds a field called `unsetFields`. It takes an array of field names, and it will set all of those fields
|
269
|
+
to null.
|
270
|
+
|
271
|
+
If the field is not nullable in the database, or it has an unconditional presence validator, you cannot pass it to `unsetFields`.
|
272
|
+
Also, if _all_ of the fields meet this criteria, the gem does not even create the `unsetFields` field.
|
273
|
+
|
274
|
+
The important side-effect here is that `leave_unchanged` causes all of the input fields on the mutation to be nullable.
|
275
|
+
|
276
|
+
### Mutations and has_many associations
|
277
|
+
You can create mutations that update models across a `has_many` association, by using a `nested` block just like you would for
|
278
|
+
`has_one` or `belongs_to` associations:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
nested :emergency_contacts, null_behavior: :set_null do
|
282
|
+
attr :first_name
|
283
|
+
attr :last_name
|
284
|
+
attr :phone
|
285
|
+
end
|
286
|
+
```
|
287
|
+
|
288
|
+
By default, inputs are matched to associated models by position (ie, the first input to the first model, etc). However, if you
|
289
|
+
have an attribute that should instead be used to match them, you can specify it:
|
290
|
+
```ruby
|
291
|
+
nested :emergency_contacts, find_by: :priority, null_behavior: :set_null do
|
292
|
+
attr :first_name
|
293
|
+
attr :last_name
|
294
|
+
attr :phone
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
This causes the gem to automatically include `priority` as an input field. Unfortunately, the only field you _can't_ use is the
|
299
|
+
`id` field, because the gem isn't smart enough to map global ID's to model ID's. (This will be supported eventually, though.)
|
300
|
+
|
301
|
+
Also, an important note is that the gem assumes that you are passing up _all_ of the associated models, and not just some of
|
302
|
+
them. It will destroy extra models, or create missing models.
|
303
|
+
|
304
|
+
### Other things that need to be documented
|
305
|
+
- Custom scalar types
|
306
|
+
- `object_to_model`
|
307
|
+
- Retrieving field metadata (for building an authorization middleware)
|
308
|
+
- Validation error exceptions
|
309
|
+
- Why all fields on query types are nullable
|
310
|
+
|
24
311
|
|
25
312
|
## Development
|
26
313
|
|
27
|
-
TODO: Write development instructions here
|
314
|
+
TODO: Write development instructions here 😬
|
315
|
+
|
316
|
+
Current goals:
|
317
|
+
- RSpec tests. Requires getting a dummy schema in place.
|
318
|
+
- Clean up awkward integration points (ie, global node identification)
|
319
|
+
- Deprecate and remove relation loader code
|
320
|
+
- Compatibility with latest version of `graphql` gem
|
28
321
|
|
29
322
|
## Contributing
|
30
323
|
|
31
324
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/graphql-activerecord. 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.
|
32
325
|
|
33
|
-
|
34
326
|
## License
|
35
327
|
|
36
328
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -19,10 +19,10 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
|
-
spec.add_runtime_dependency "activesupport", "
|
23
|
-
spec.add_runtime_dependency "activerecord", "
|
24
|
-
spec.add_runtime_dependency "graphql", "
|
25
|
-
spec.add_runtime_dependency "graphql-batch", "
|
22
|
+
spec.add_runtime_dependency "activesupport", ">= 4.2", '< 5'
|
23
|
+
spec.add_runtime_dependency "activerecord", ">= 4.2", '< 5'
|
24
|
+
spec.add_runtime_dependency "graphql", ">= 0.18.4"
|
25
|
+
spec.add_runtime_dependency "graphql-batch", ">= 0.2.4"
|
26
26
|
|
27
27
|
spec.add_development_dependency "bundler", "~> 1.11"
|
28
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
metadata
CHANGED
@@ -1,69 +1,81 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-activerecord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.0
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Foster
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '4.2'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '4.2'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: activerecord
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
|
-
- - "
|
37
|
+
- - ">="
|
32
38
|
- !ruby/object:Gem::Version
|
33
39
|
version: '4.2'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '5'
|
34
43
|
type: :runtime
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
37
46
|
requirements:
|
38
|
-
- - "
|
47
|
+
- - ">="
|
39
48
|
- !ruby/object:Gem::Version
|
40
49
|
version: '4.2'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '5'
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: graphql
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|
44
56
|
requirements:
|
45
|
-
- - "
|
57
|
+
- - ">="
|
46
58
|
- !ruby/object:Gem::Version
|
47
59
|
version: 0.18.4
|
48
60
|
type: :runtime
|
49
61
|
prerelease: false
|
50
62
|
version_requirements: !ruby/object:Gem::Requirement
|
51
63
|
requirements:
|
52
|
-
- - "
|
64
|
+
- - ">="
|
53
65
|
- !ruby/object:Gem::Version
|
54
66
|
version: 0.18.4
|
55
67
|
- !ruby/object:Gem::Dependency
|
56
68
|
name: graphql-batch
|
57
69
|
requirement: !ruby/object:Gem::Requirement
|
58
70
|
requirements:
|
59
|
-
- - "
|
71
|
+
- - ">="
|
60
72
|
- !ruby/object:Gem::Version
|
61
73
|
version: 0.2.4
|
62
74
|
type: :runtime
|
63
75
|
prerelease: false
|
64
76
|
version_requirements: !ruby/object:Gem::Requirement
|
65
77
|
requirements:
|
66
|
-
- - "
|
78
|
+
- - ">="
|
67
79
|
- !ruby/object:Gem::Version
|
68
80
|
version: 0.2.4
|
69
81
|
- !ruby/object:Gem::Dependency
|
@@ -133,6 +145,7 @@ files:
|
|
133
145
|
- ".gitignore"
|
134
146
|
- ".rspec"
|
135
147
|
- ".travis.yml"
|
148
|
+
- CHANGELOG.md
|
136
149
|
- CODE_OF_CONDUCT.md
|
137
150
|
- Gemfile
|
138
151
|
- LICENSE.txt
|
@@ -195,9 +208,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
195
208
|
version: '0'
|
196
209
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
210
|
requirements:
|
198
|
-
- - "
|
211
|
+
- - ">="
|
199
212
|
- !ruby/object:Gem::Version
|
200
|
-
version:
|
213
|
+
version: '0'
|
201
214
|
requirements: []
|
202
215
|
rubyforge_project:
|
203
216
|
rubygems_version: 2.4.8
|