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 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