graphql-activerecord 0.8.0.pre.alpha1 → 0.8.0
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 +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
|