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