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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a18a5d24f96c94faa11072b275606e9160f6ecb
4
- data.tar.gz: fc4d9b955200937dddc40a03ed00ba8f063e5eb7
3
+ metadata.gz: 6cc30cdefc71e9c7880e6f49ed3a4817f69ab3ca
4
+ data.tar.gz: 25d052ad19f4b048ab4498afadf0c4472b1ec974
5
5
  SHA512:
6
- metadata.gz: a34c305a0ccd72210795eb982206e53e6bf3c094339bc0bf62ab501eea5f41b62ed2860caa06b12c194fae102e64d9e71946bd7c68c4d4a4c73d5c122fd31e39
7
- data.tar.gz: f1d52a786acc02b0e6f9c406ea4a1cebaa23c29d2760a3a5bc02122ff3b68475d2b95f8eb70409323fe35736e1dca9bafa67d198d2ac74a380d5f931c6ab6e25
6
+ metadata.gz: e5722b4fa3cd5a4dc07b868569e37213ba0c8552b5a17057c7859423c2ca8c9ad846796b6efa04f4bf98ec616d21f3bde880234a3655fd6b4f68ba0dd63b698f
7
+ data.tar.gz: 3c9ef697f0f9f16db19b80eb9bcdfeda3ad71bb46371cb497875b48a3bbdc8da159c6ef15f58e5bf612d7684d68ceea5dd58dbd7c23c7d8eb05fcbd7c091dfd1
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.7.2
4
+
5
+ ### Breaking Changes
6
+ - Changed models are no longer reloaded when you call `save!` on a mutator
data/README.md CHANGED
@@ -1,10 +1,104 @@
1
1
  # GraphQL::Models
2
2
 
3
- TODO: Write this. I promise, one day, I really will write some stuff here to describe this gem.
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
- Add this line to your application's Gemfile:
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
- Or install it yourself as:
146
+ ### Database compatibility
18
147
 
19
- $ gem install graphql-activerecord
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
- TODO: Write usage instructions here
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", "~> 4.2"
23
- spec.add_runtime_dependency "activerecord", "~> 4.2"
24
- spec.add_runtime_dependency "graphql", "~> 0.18.4"
25
- spec.add_runtime_dependency "graphql-batch", "~> 0.2.4"
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"
@@ -44,7 +44,7 @@ module GraphQL::Models
44
44
  end
45
45
  end
46
46
 
47
- changed_models.reject(&:destroyed?).each(&:reload)
47
+ changed_models.reject(&:destroyed?)
48
48
  end
49
49
  end
50
50
  end
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module Models
3
- VERSION = "0.8.0-alpha1"
3
+ VERSION = "0.8.0"
4
4
  end
5
5
  end
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.pre.alpha1
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-08-31 00:00:00.000000000 Z
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: 1.3.1
213
+ version: '0'
201
214
  requirements: []
202
215
  rubyforge_project:
203
216
  rubygems_version: 2.4.8