graphql-activerecord 0.12.6 → 0.13.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.
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
data/bin/rspec 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
 
@@ -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
 
@@ -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,6 +1,6 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
- lib = File.expand_path('../lib', __FILE__)
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'graphql/models/version'
6
6
 
@@ -20,14 +20,14 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.add_runtime_dependency "activesupport", ">= 4.2", '< 6'
24
23
  spec.add_runtime_dependency "activerecord", ">= 4.2", '< 6'
25
- spec.add_runtime_dependency "graphql", ">= 1.5.10", '< 2'
24
+ spec.add_runtime_dependency "activesupport", ">= 4.2", '< 6'
25
+ spec.add_runtime_dependency "graphql", ">= 1.7.5", '< 2'
26
26
  spec.add_runtime_dependency "graphql-batch", ">= 0.2.4"
27
27
 
28
28
  spec.add_development_dependency "bundler", "~> 1.11"
29
+ spec.add_development_dependency "pry"
29
30
  spec.add_development_dependency "rake", "~> 10.0"
30
31
  spec.add_development_dependency "rspec", "~> 3.0"
31
- spec.add_development_dependency "pry"
32
- spec.add_development_dependency "rubocop", '~> 0.47.1'
32
+ spec.add_development_dependency "rubocop"
33
33
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'active_support'
3
4
  require 'active_record'
4
5
  require 'graphql'
@@ -39,6 +40,7 @@ module GraphQL
39
40
  module Models
40
41
  class << self
41
42
  attr_accessor :model_from_id, :authorize, :id_for_model, :model_to_graphql_type, :unknown_scalar
43
+ attr_accessor :legacy_nulls
42
44
  end
43
45
 
44
46
  # Returns a promise that will traverse the associations and resolve to the model at the end of the path.
@@ -72,11 +74,13 @@ module GraphQL
72
74
  authorize.call(context, model, action)
73
75
  end
74
76
 
75
- def self.define_mutator(definer, model_type, null_behavior:, &block)
77
+ def self.define_mutator(definer, model_type, null_behavior: :leave_unchanged, legacy_nulls: GraphQL::Models.legacy_nulls, &block)
78
+ legacy_nulls ||= false
79
+
76
80
  # HACK: To get the name of the mutation, to avoid possible collisions with other type names
77
81
  prefix = definer.instance_variable_get(:@target).name
78
82
 
79
- mutator_definition = MutatorDefinition.new(model_type, null_behavior: null_behavior)
83
+ mutator_definition = MutatorDefinition.new(model_type, null_behavior: null_behavior, legacy_nulls: legacy_nulls)
80
84
  mutator_definition.field_map.instance_exec(&block)
81
85
  MutationHelpers.print_input_fields(mutator_definition.field_map, definer, "#{prefix}Input")
82
86
  mutator_definition
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Models
4
5
  module ActiveRecordExtension
@@ -24,7 +25,7 @@ module GraphQL
24
25
  extend ::ActiveSupport::Concern
25
26
  class_methods do
26
27
  def graphql_enum_types
27
- @_graphql_enum_types ||= EnumTypeHash.new
28
+ @graphql_enum_types ||= EnumTypeHash.new
28
29
  end
29
30
 
30
31
  # Defines a GraphQL enum type on the model
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Models
4
5
  class AssociationLoadRequest
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  # Simplified loader that can take a hash of attributes to match, combine them into a single query, and then fulfill
4
5
  # then individually. It can also ask the database to order them correctly.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Models
4
5
  class BackedByModel
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Models
4
5
  module DatabaseTypes
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # This is a helper class. It lets you build simple DSL's. Methods called against the class are
3
4
  # converted into attributes in a hash.
4
5
  module GraphQL
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'ostruct'
3
4
 
4
5
  module GraphQL
@@ -66,7 +67,7 @@ module GraphQL
66
67
  return false unless context
67
68
 
68
69
  reflection = association.reflection
69
- return false unless [:has_one, :belongs_to].include?(reflection.macro)
70
+ return false unless %i[has_one belongs_to].include?(reflection.macro)
70
71
 
71
72
  if reflection.macro == :belongs_to
72
73
  target_id = model.send(reflection.foreign_key)
@@ -123,7 +124,7 @@ module GraphQL
123
124
  field_name = field_name.to_s
124
125
 
125
126
  field_meta = graph_type.instance_variable_get(:@field_metadata)
126
- field_meta = graph_type.instance_variable_set(:@field_metadata, {}) unless field_meta
127
+ field_meta ||= graph_type.instance_variable_set(:@field_metadata, {})
127
128
  field_meta[field_name] = OpenStruct.new(metadata).freeze
128
129
  end
129
130
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Models
4
5
  module DefinitionHelpers
@@ -6,7 +7,7 @@ module GraphQL
6
7
  reflection = model_type.reflect_on_association(association)
7
8
  raise ArgumentError, "Association #{association} wasn't found on model #{model_type.name}" unless reflection
8
9
  raise ArgumentError, "Cannot proxy to polymorphic association #{association} on model #{model_type.name}" if reflection.polymorphic?
9
- raise ArgumentError, "Cannot proxy to #{reflection.macro} association #{association} on model #{model_type.name}" unless [:has_one, :belongs_to].include?(reflection.macro)
10
+ raise ArgumentError, "Cannot proxy to #{reflection.macro} association #{association} on model #{model_type.name}" unless %i[has_one belongs_to].include?(reflection.macro)
10
11
 
11
12
  return unless block_given?
12
13
 
@@ -56,7 +57,7 @@ module GraphQL
56
57
  reflection = model_type.reflect_on_association(association)
57
58
 
58
59
  raise ArgumentError, "Association #{association} wasn't found on model #{model_type.name}" unless reflection
59
- raise ArgumentError, "Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_one" unless [:has_one, :belongs_to].include?(reflection.macro)
60
+ raise ArgumentError, "Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_one" unless %i[has_one belongs_to].include?(reflection.macro)
60
61
 
61
62
  # Define the field for the association itself
62
63
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Models
4
5
  module DefinitionHelpers
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models::HashCombiner
3
4
  class << self
4
5
  # Takes a set of hashes that represent conditions, and combines them into the smallest number of hashes
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  module Helpers
4
5
  def self.orders_to_sql(orders)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class GraphQL::Models::Instrumentation
3
4
  # @param skip_nil_models If true, field resolvers (in proxy_to or backed_by_model blocks) will not be invoked if the model is nil.
4
5
  def initialize(skip_nil_models = true)
@@ -12,10 +13,11 @@ class GraphQL::Models::Instrumentation
12
13
  old_resolver = field.resolve_proc
13
14
 
14
15
  new_resolver = -> (object, args, ctx) {
15
- base_model = field_info.object_to_base_model.call(object)
16
- GraphQL::Models.load_association(base_model, field_info.path, ctx).then do |model|
17
- next nil if model.nil? && @skip_nil_models
18
- old_resolver.call(model, args, ctx)
16
+ Promise.resolve(field_info.object_to_base_model.call(object)).then do |base_model|
17
+ GraphQL::Models.load_association(base_model, field_info.path, ctx).then do |model|
18
+ next nil if model.nil? && @skip_nil_models
19
+ old_resolver.call(model, args, ctx)
20
+ end
19
21
  end
20
22
  }
21
23
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class GraphQL::Query::Context
3
4
  def cached_models
4
5
  @cached_models ||= Set.new
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  class MutationFieldMap
4
- attr_accessor :model_type, :find_by, :null_behavior, :fields, :nested_maps
5
+ attr_accessor :model_type, :find_by, :null_behavior, :fields, :nested_maps, :legacy_nulls
5
6
 
6
7
  # These are used when this is a proxy_to or a nested field map
7
8
  attr_accessor :name, :association, :has_many, :required, :path
8
9
 
9
- def initialize(model_type, find_by:, null_behavior:)
10
+ def initialize(model_type, find_by:, null_behavior:, legacy_nulls:)
10
11
  raise ArgumentError, "model_type must be a model" if model_type && !(model_type <= ActiveRecord::Base)
11
- raise ArgumentError, "null_behavior must be :set_null or :leave_unchanged" unless [:set_null, :leave_unchanged].include?(null_behavior)
12
+ raise ArgumentError, "null_behavior must be :set_null or :leave_unchanged" unless %i[set_null leave_unchanged].include?(null_behavior)
12
13
 
13
14
  @fields = []
14
15
  @nested_maps = []
@@ -16,6 +17,7 @@ module GraphQL::Models
16
17
  @model_type = model_type
17
18
  @find_by = Array.wrap(find_by)
18
19
  @null_behavior = null_behavior
20
+ @legacy_nulls = legacy_nulls
19
21
 
20
22
  @find_by.each { |f| attr(f) }
21
23
  end
@@ -65,7 +67,7 @@ module GraphQL::Models
65
67
  reflection = model_type&.reflect_on_association(association)
66
68
 
67
69
  if reflection
68
- unless [:belongs_to, :has_one].include?(reflection.macro)
70
+ unless %i[belongs_to has_one].include?(reflection.macro)
69
71
  raise ArgumentError, "Cannot proxy to #{reflection.macro} association #{association} from #{model_type.name}"
70
72
  end
71
73
 
@@ -74,7 +76,7 @@ module GraphQL::Models
74
76
  klass = nil
75
77
  end
76
78
 
77
- proxy = MutationFieldMap.new(klass, find_by: nil, null_behavior: null_behavior)
79
+ proxy = MutationFieldMap.new(klass, find_by: nil, null_behavior: null_behavior, legacy_nulls: legacy_nulls)
78
80
  proxy.association = association
79
81
  proxy.instance_exec(&block)
80
82
 
@@ -97,7 +99,7 @@ module GraphQL::Models
97
99
  end
98
100
  end
99
101
 
100
- def nested(association, find_by: nil, null_behavior:, name: nil, has_many: false, &block)
102
+ def nested(association, find_by: nil, null_behavior:, name: nil, &block)
101
103
  unless model_type
102
104
  raise ArgumentError, "Cannot use `nested` unless the model type is known at build time."
103
105
  end
@@ -116,7 +118,7 @@ module GraphQL::Models
116
118
  has_many = reflection.macro == :has_many
117
119
  required = Reflection.is_required(model_type, association)
118
120
 
119
- map = MutationFieldMap.new(reflection.klass, find_by: find_by, null_behavior: null_behavior)
121
+ map = MutationFieldMap.new(reflection.klass, find_by: find_by, null_behavior: null_behavior, legacy_nulls: legacy_nulls)
120
122
  map.name = name || association.to_s.camelize(:lower)
121
123
  map.association = association.to_s
122
124
  map.has_many = has_many
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  module MutationHelpers
4
5
  def self.apply_changes(field_map, model, inputs, context)
@@ -7,7 +8,13 @@ module GraphQL::Models
7
8
 
8
9
  # Values will now contain the list of inputs that we should actually act on. Any null values should actually
9
10
  # be set to null, and missing fields should be skipped.
10
- values = field_map.leave_null_unchanged? ? prep_leave_unchanged(inputs) : prep_set_null(field_map, inputs)
11
+ values = if field_map.leave_null_unchanged? && field_map.legacy_nulls
12
+ prep_legacy_leave_unchanged(inputs)
13
+ elsif field_map.leave_null_unchanged?
14
+ prep_leave_unchanged(inputs)
15
+ else
16
+ prep_set_null(field_map, inputs)
17
+ end
11
18
 
12
19
  values.each do |name, value|
13
20
  field_def = field_map.fields.detect { |f| f[:name] == name }
@@ -232,10 +239,10 @@ module GraphQL::Models
232
239
  end
233
240
  end
234
241
 
235
- # If the field map has the option leave_null_unchanged, there's an `unsetFields` string array that contains the
236
- # name of inputs that should be treated as if they are null. We handle that by removing null inputs, and then
237
- # adding back any unsetFields with null values.
238
- def self.prep_leave_unchanged(inputs)
242
+ # If the field map has the option leave_null_unchanged, and legacy_nulls is true, then there's an `unsetFields` string
243
+ # array that contains the name of inputs that should be treated as if they are null. We handle that by removing null
244
+ # inputs, and then adding back any unsetFields with null values.
245
+ def self.prep_legacy_leave_unchanged(inputs)
239
246
  # String key hash
240
247
  values = inputs.to_h.compact
241
248
 
@@ -249,6 +256,10 @@ module GraphQL::Models
249
256
  values
250
257
  end
251
258
 
259
+ def self.prep_leave_unchanged(inputs)
260
+ inputs.to_h
261
+ end
262
+
252
263
  # Field map has the option to set_null. Any field that has the value null, or is missing, will be set to null.
253
264
  def self.prep_set_null(field_map, inputs)
254
265
  values = inputs.to_h.compact
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  module MutationHelpers
4
5
  def self.authorize_changes(context, all_changes)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  module MutationHelpers
4
5
  def self.print_input_fields(field_map, definer, map_name_prefix)
@@ -7,14 +8,14 @@ module GraphQL::Models
7
8
  field_type = f[:type]
8
9
 
9
10
  if f[:required] && !field_map.leave_null_unchanged?
10
- field_type = field_type.to_non_null_type
11
+ field_type = field_type.to_non_null_type unless field_type.non_null?
11
12
  end
12
13
 
13
14
  input_field(f[:name], field_type)
14
15
  end
15
16
 
16
- if field_map.leave_null_unchanged?
17
- field_names = field_map.fields.select { |f| !f[:required] }.map { |f| f[:name].to_s }
17
+ if field_map.leave_null_unchanged? && field_map.legacy_nulls
18
+ field_names = field_map.fields.reject { |f| f[:required] }.map { |f| f[:name].to_s }
18
19
  field_names += field_map.nested_maps.reject(&:required).map { |fld| fld.name.to_s }
19
20
  field_names = field_names.sort
20
21
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL::Models
3
4
  module MutationHelpers
4
5
  def self.validate_changes(inputs, field_map, root_model, context, all_changes)