graphql-activerecord 0.12.6 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +581 -11
  3. data/CHANGELOG.md +6 -0
  4. data/Gemfile +1 -0
  5. data/README.md +69 -37
  6. data/Rakefile +1 -0
  7. data/bin/bundle +105 -0
  8. data/bin/coderay +12 -0
  9. data/bin/htmldiff +12 -0
  10. data/bin/ldiff +12 -0
  11. data/bin/pry +12 -0
  12. data/bin/rake +12 -0
  13. data/bin/rspec +12 -0
  14. data/bin/rubocop +12 -0
  15. data/bin/ruby-parse +12 -0
  16. data/bin/ruby-rewrite +12 -0
  17. data/graphql-activerecord.gemspec +6 -6
  18. data/lib/graphql/activerecord.rb +6 -2
  19. data/lib/graphql/models/active_record_extension.rb +2 -1
  20. data/lib/graphql/models/association_load_request.rb +1 -0
  21. data/lib/graphql/models/attribute_loader.rb +1 -0
  22. data/lib/graphql/models/backed_by_model.rb +1 -0
  23. data/lib/graphql/models/database_types.rb +1 -0
  24. data/lib/graphql/models/definer.rb +1 -0
  25. data/lib/graphql/models/definition_helpers.rb +3 -2
  26. data/lib/graphql/models/definition_helpers/associations.rb +3 -2
  27. data/lib/graphql/models/definition_helpers/attributes.rb +1 -0
  28. data/lib/graphql/models/hash_combiner.rb +1 -0
  29. data/lib/graphql/models/helpers.rb +1 -0
  30. data/lib/graphql/models/instrumentation.rb +6 -4
  31. data/lib/graphql/models/monkey_patches/graphql_query_context.rb +1 -0
  32. data/lib/graphql/models/mutation_field_map.rb +9 -7
  33. data/lib/graphql/models/mutation_helpers/apply_changes.rb +16 -5
  34. data/lib/graphql/models/mutation_helpers/authorization.rb +1 -0
  35. data/lib/graphql/models/mutation_helpers/print_input_fields.rb +4 -3
  36. data/lib/graphql/models/mutation_helpers/validation.rb +1 -0
  37. data/lib/graphql/models/mutation_helpers/validation_error.rb +1 -0
  38. data/lib/graphql/models/mutator.rb +3 -2
  39. data/lib/graphql/models/promise_relation_connection.rb +1 -0
  40. data/lib/graphql/models/reflection.rb +2 -0
  41. data/lib/graphql/models/relation_load_request.rb +1 -0
  42. data/lib/graphql/models/relation_loader.rb +1 -0
  43. data/lib/graphql/models/version.rb +2 -1
  44. metadata +23 -22
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ # 0.13.0
4
+ Changed the way that null values are handled inside of mutators. Take a look at [(#49)](https://github.com/goco-inc/graphql-activerecord/pull/49)
5
+ for details. If you need to get back to the old behavior (ie, `unsetFields`), you can either:
6
+ - Add the `legacy_nulls: true` option when defining your mutator, or
7
+ - Set `GraphQL::Models.legacy_nulls = true` in an initializer
8
+
3
9
  # 0.12.6
4
10
  - Fixed a bug when you used a `nested` mutator, and provided a symbol for the `:name` kwarg
5
11
  - Fixed a bug where the `context` parameter was not being passed to `MutationHelpers::match_inputs_to_models`
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  source 'https://rubygems.org'
3
4
 
4
5
  # Specify your gem's dependencies in graphql-activerecord.gemspec
data/README.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # GraphQL::Models
2
2
 
3
+ ## WARNING!
4
+ This gem was designed as a helper for building GraphQL schema's based on the [`graphql`](https://github.com/rmosolgo/graphql-ruby) gem. Primarily, it was meant to reduce redundancy when your object types were virtually identical to the attributes on your models in a few ways:
5
+ 1. It uses some clever tricks to automatically infer field types based on the data types of your database columns
6
+ 2. It automatically camelizes your attributes names
7
+ 3. It has some helpers to optimize association loading
8
+
9
+ But in the time since I originally wrote this gem, a lot has transpired in the GraphQL world:
10
+ - For #2: The 1.8 release of the graphql gem solves field camelization
11
+ - For #3: Better solutions for association loading, that aren't quite as heavy as this library, have surfaced. One example is outlined in a [gist](https://gist.github.com/theorygeek/a1a59a2bf9c59e4b3706ac68d12c8434) that I wrote on Association Loading. That gist has proven to be more popular than this library 😁
12
+
13
+ I don't know if there's a good solution out there for #1, but it was probably the least important problem to solve.
14
+
15
+ We use GraphQL extensively at GoCo. Our schema has thousands of types. We'll be rethinking our implementation soon, and taking a closer look at the patterns that we use to DRY up our schema definition, to see if there are better patterns in the 1.8+ world.
16
+
17
+ This gem may evolve into something better, or we may eventually deprecate it. But as of right now, I can't recommend that you build any major projects on top of it, since its future is a bit uncertain. I apologize if that makes more work for you :( but I want to be honest about the state of the project.
18
+
19
+ ## Overview
20
+
3
21
  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)
4
22
  gem. It assumes that you're using Rails and have `graphql-batch` set up.
5
23
 
@@ -11,7 +29,7 @@ In the process, this gem also converts `snake_case` attribute names into `camelC
11
29
 
12
30
  Here's an example:
13
31
  ```ruby
14
- EmployeeGraph = GraphQL::ObjectType.define do
32
+ EmployeeType = GraphQL::ObjectType.define do
15
33
  name "Employee"
16
34
 
17
35
  # Looks for an Active Record model called "Employee"
@@ -38,6 +56,22 @@ EmployeeGraph = GraphQL::ObjectType.define do
38
56
  end
39
57
  ```
40
58
 
59
+ Then in your query file:
60
+ ```ruby
61
+ QueryType = GraphQL::ObjectType.define do
62
+ name "Query"
63
+
64
+ field :employees, types[EmployeeType] do
65
+ resolve -> (_obj, _args, _ctx) { Employee.all }
66
+ end
67
+
68
+ field :employee, EmployeeType do
69
+ argument :id, !types.ID
70
+ resolve -> (obj, args, ctx) { Employee.find(args[:id])}
71
+ end
72
+ end
73
+ ```
74
+
41
75
  You can also build a corresponding mutation, using a very similar syntax. Mutations are more complicated, since they involve
42
76
  not just changing the data, but also validating it. Here's an example:
43
77
  ```ruby
@@ -49,7 +83,7 @@ UpdateEmployeeMutation = GraphQL::Relay::Mutation.define do
49
83
  # For mutations, you create a mutator definition. This will add the input fields to your
50
84
  # mutation, and also return an object that you'll use in the resolver to perform the mutation.
51
85
  # The parameters you pass are explained below.
52
- mutator_definition = GraphQL::Models.define_mutator(self, Employee, null_behavior: :leave_unchanged) do
86
+ mutator_definition = GraphQL::Models.define_mutator(self, Employee) do
53
87
  attr :title
54
88
  attr :salary
55
89
 
@@ -60,7 +94,7 @@ UpdateEmployeeMutation = GraphQL::Relay::Mutation.define do
60
94
 
61
95
  # You can use nested input object types to allow making changes across associations with a single mutation.
62
96
  # Unlike querying, you need to be explicit about what fields on associated objects can be changed.
63
- nested :address, null_behavior: :set_null do
97
+ nested :address do
64
98
  attr :line_1
65
99
  attr :line_2
66
100
  attr :city
@@ -70,7 +104,7 @@ UpdateEmployeeMutation = GraphQL::Relay::Mutation.define do
70
104
  end
71
105
  end
72
106
 
73
- return_field :employee, EmployeeGraph
107
+ return_field :employee, EmployeeType
74
108
 
75
109
  resolve -> (inputs, context) {
76
110
  # Fetch (or create) the model that the mutation should change
@@ -96,6 +130,15 @@ UpdateEmployeeMutation = GraphQL::Relay::Mutation.define do
96
130
  end
97
131
  ```
98
132
 
133
+ In your mutation file:
134
+ ```ruby
135
+ MutationType = GraphQL::ObjectType.define do
136
+ name "Mutation"
137
+
138
+ field :updateEmployee, UpdateEmployeeMutation.field
139
+ end
140
+ ```
141
+
99
142
  ## Installation
100
143
 
101
144
  To get started, you should add this line to your application's Gemfile:
@@ -132,15 +175,14 @@ GraphQL::Models.authorize = -> (context, action, model) {
132
175
 
133
176
  # The gem assumes that if your model is called `MyModel`, the corresponding type is `MyModelType`.
134
177
  # You can override that convention. Return `nil` if the model doesn't have a GraphQL type:
135
- GraphQL::Models.model_to_graphql_type = -> (model_class) { "#{model_class.name}Graph".safe_constantize }
178
+ GraphQL::Models.model_to_graphql_type = -> (model_class) { "#{model_class.name}Type".safe_constantize }
136
179
  ```
137
180
 
138
181
  Finally, you need to set a few options on your schema:
139
182
  ```ruby
140
183
  GraphQL::Schema.define do
141
184
  # Set up the graphql-batch gem
142
- lazy_resolve(Promise, :sync)
143
- instrument(:query, GraphQL::Batch::Setup)
185
+ use GraphQL::Batch
144
186
 
145
187
  # Set up the graphql-activerecord gem
146
188
  instrument(:field, GraphQL::Models::Instrumentation.new)
@@ -309,7 +351,7 @@ end
309
351
  MyModel.graphql_enum_types[:status]
310
352
 
311
353
  # When you use it inside of your GraphQL schema, it'll know to use the GraphQL enum type:
312
- MyModelGraph = GraphQL::ObjectType.define do
354
+ MyModelType = GraphQL::ObjectType.define do
313
355
  backed_by_model :my_model do
314
356
  attr :status
315
357
  end
@@ -326,13 +368,12 @@ You can also manually specify the type to use, if you just want the type mapping
326
368
  When you define a mutation, there are a few parameters that you need to pass. Here's an example:
327
369
 
328
370
  ```ruby
329
- mutator_definition = GraphQL::Models.define_mutator(self, Employee, null_behavior: :leave_unchanged)
371
+ mutator_definition = GraphQL::Models.define_mutator(self, Employee)
330
372
  ```
331
373
 
332
374
  The parameters are:
333
375
  - The definer object: it needs this so that it can create the input fields. You should always pass `self` for this parameter.
334
376
  - The model class that the mutator is changing: it needs this so that it can map attributes to the correct input types.
335
- - `null_behavior`: this lets you choose how null values are treated. It's explained below.
336
377
 
337
378
  #### Virtual Attributes
338
379
  In your mutator, you can specify virtual attributes on your model, you just need to provide the type:
@@ -340,39 +381,30 @@ In your mutator, you can specify virtual attributes on your model, you just need
340
381
  attr :some_fake_attribute, type: types.String
341
382
  ```
342
383
 
343
- #### null_behavior
344
-
345
- When you build a mutation, you have two options that control how null values are treated. They are meant to allow you
346
- 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
347
- values for every field.
348
-
349
- You specify which option you'd like using the `null_behavior` parameter when defining the mutation:
350
- - `leave_unchanged` means that if the input field contains a null value, it is ignored
351
- - `set_null` means that if the input field contains a null value, the attribute will actually be set to `nil`
384
+ #### Implicit Null Values
352
385
 
353
- ##### set_null
354
- The `set_null` option is the simpler of the two, and you should probably default to using that option. When you pick this option,
355
- null values behave as you would expect: they cause the attribute to be set to `nil`.
386
+ By default, input fields that are not supplied to a mutation (ie, they are left blank when the mutation is executed) will
387
+ be ignored. You must explicitly provide a value (including `null`) for the attribute to be updated.
356
388
 
357
- But another important side-effect is that the input fields on the mutation will be marked as non-nullable if the underlying
358
- database column is not nullable, or if there is an unconditional presence validator on that field.
389
+ You can override this behavior by using the `null_behavior: :set_null` option. This will cause two side-effects:
390
+ - The input fields on your mutation will be marked non-null if they are required in your model
391
+ - If any input field is not supplied, it will be treated as if the value `null` was actually supplied.
359
392
 
360
- ##### leave_unchanged
361
- If you select this option, any input fields that contain null values will be ignored. Instead, if you really do want to set a
362
- 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
363
- to null.
364
-
365
- If the field is not nullable in the database, or it has an unconditional presence validator, you cannot pass it to `unsetFields`.
366
- Also, if _all_ of the fields meet this criteria, the gem does not even create the `unsetFields` field.
367
-
368
- The important side-effect here is that `leave_unchanged` causes all of the input fields on the mutation to be nullable.
393
+ Example:
394
+ ```ruby
395
+ nested :emergency_contacts, null_behavior: :set_null do
396
+ attr :first_name
397
+ attr :last_name
398
+ attr :phone
399
+ end
400
+ ```
369
401
 
370
402
  ### Mutations and has_many associations
371
403
  You can create mutations that update models across a `has_many` association, by using a `nested` block just like you would for
372
404
  `has_one` or `belongs_to` associations:
373
405
 
374
406
  ```ruby
375
- nested :emergency_contacts, null_behavior: :set_null do
407
+ nested :emergency_contacts do
376
408
  attr :first_name
377
409
  attr :last_name
378
410
  attr :phone
@@ -382,7 +414,7 @@ end
382
414
  By default, inputs are matched to associated models by position (ie, the first input to the first model, etc). However, if you
383
415
  have an attribute that should instead be used to match them, you can specify it:
384
416
  ```ruby
385
- nested :emergency_contacts, find_by: :priority, null_behavior: :set_null do
417
+ nested :emergency_contacts, find_by: :priority do
386
418
  attr :first_name
387
419
  attr :last_name
388
420
  attr :phone
@@ -392,8 +424,8 @@ end
392
424
  This causes the gem to automatically include `priority` as an input field. You could also manually specify the
393
425
  `priority` field if you wanted to override its name or type.
394
426
 
395
- Also, an important note is that the gem assumes that you are passing up _all_ of the associated models, and not just some of
396
- them. It will destroy extra models, or create missing models.
427
+ Also, an important note is that the gem assumes that your input is providing values for _all_ of the associated models, and not just
428
+ some of them. It will destroy extra models, or create missing models.
397
429
 
398
430
  ### Other things that need to be documented
399
431
  - Custom scalar types
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "bundler/gem_tasks"
3
4
  require "rspec/core/rake_task"
4
5
 
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "rubygems"
12
+
13
+ m = Module.new do
14
+ module_function
15
+
16
+ def invoked_as_script?
17
+ File.expand_path($0) == File.expand_path(__FILE__)
18
+ end
19
+
20
+ def env_var_version
21
+ ENV["BUNDLER_VERSION"]
22
+ end
23
+
24
+ def cli_arg_version
25
+ return unless invoked_as_script? # don't want to hijack other binstubs
26
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27
+ bundler_version = nil
28
+ update_index = nil
29
+ ARGV.each_with_index do |a, i|
30
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31
+ bundler_version = a
32
+ end
33
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
+ bundler_version = $1 || ">= 0.a"
35
+ update_index = i
36
+ end
37
+ bundler_version
38
+ end
39
+
40
+ def gemfile
41
+ gemfile = ENV["BUNDLE_GEMFILE"]
42
+ return gemfile if gemfile && !gemfile.empty?
43
+
44
+ File.expand_path("../../Gemfile", __FILE__)
45
+ end
46
+
47
+ def lockfile
48
+ lockfile =
49
+ case File.basename(gemfile)
50
+ when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51
+ else "#{gemfile}.lock"
52
+ end
53
+ File.expand_path(lockfile)
54
+ end
55
+
56
+ def lockfile_version
57
+ return unless File.file?(lockfile)
58
+ lockfile_contents = File.read(lockfile)
59
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60
+ Regexp.last_match(1)
61
+ end
62
+
63
+ def bundler_version
64
+ @bundler_version ||= begin
65
+ env_var_version || cli_arg_version ||
66
+ lockfile_version || "#{Gem::Requirement.default}.a"
67
+ end
68
+ end
69
+
70
+ def load_bundler!
71
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
72
+
73
+ # must dup string for RG < 1.8 compatibility
74
+ activate_bundler(bundler_version.dup)
75
+ end
76
+
77
+ def activate_bundler(bundler_version)
78
+ if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
79
+ bundler_version = "< 2"
80
+ end
81
+ gem_error = activation_error_handling do
82
+ gem "bundler", bundler_version
83
+ end
84
+ return if gem_error.nil?
85
+ require_error = activation_error_handling do
86
+ require "bundler/version"
87
+ end
88
+ return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION))
89
+ warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`"
90
+ exit 42
91
+ end
92
+
93
+ def activation_error_handling
94
+ yield
95
+ nil
96
+ rescue StandardError, LoadError => e
97
+ e
98
+ end
99
+ end
100
+
101
+ m.load_bundler!
102
+
103
+ if m.invoked_as_script?
104
+ load Gem.bin_path("bundler", "bundle")
105
+ end
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  #
4
5
  # This file was generated by Bundler.
5
6
  #
@@ -11,6 +12,17 @@ require "pathname"
11
12
  ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
13
  Pathname.new(__FILE__).realpath)
13
14
 
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
14
26
  require "rubygems"
15
27
  require "bundler/setup"
16
28
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  #
4
5
  # This file was generated by Bundler.
5
6
  #
@@ -11,6 +12,17 @@ require "pathname"
11
12
  ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
13
  Pathname.new(__FILE__).realpath)
13
14
 
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
14
26
  require "rubygems"
15
27
  require "bundler/setup"
16
28
 
data/bin/ldiff CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  #
4
5
  # This file was generated by Bundler.
5
6
  #
@@ -11,6 +12,17 @@ require "pathname"
11
12
  ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
13
  Pathname.new(__FILE__).realpath)
13
14
 
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
14
26
  require "rubygems"
15
27
  require "bundler/setup"
16
28
 
data/bin/pry CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  #
4
5
  # This file was generated by Bundler.
5
6
  #
@@ -11,6 +12,17 @@ require "pathname"
11
12
  ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
13
  Pathname.new(__FILE__).realpath)
13
14
 
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
14
26
  require "rubygems"
15
27
  require "bundler/setup"
16
28
 
data/bin/rake CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  #
4
5
  # This file was generated by Bundler.
5
6
  #
@@ -11,6 +12,17 @@ require "pathname"
11
12
  ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
13
  Pathname.new(__FILE__).realpath)
13
14
 
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
14
26
  require "rubygems"
15
27
  require "bundler/setup"
16
28