graphiti 1.2.15 → 1.2.20

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +43 -14
  3. data/Appraisals +37 -5
  4. data/CHANGELOG.md +1 -1
  5. data/Gemfile +2 -0
  6. data/Guardfile +5 -5
  7. data/deprecated_generators/graphiti/resource_generator.rb +1 -1
  8. data/gemfiles/rails_5_0.gemfile +18 -0
  9. data/gemfiles/rails_5_0_graphiti_rails.gemfile +20 -0
  10. data/gemfiles/rails_5_1.gemfile +18 -0
  11. data/gemfiles/rails_5_1_graphiti_rails.gemfile +20 -0
  12. data/gemfiles/{rails_5.gemfile → rails_5_2.gemfile} +0 -0
  13. data/gemfiles/{rails_5_graphiti_rails.gemfile → rails_5_2_graphiti_rails.gemfile} +0 -0
  14. data/gemfiles/rails_6.gemfile +1 -1
  15. data/gemfiles/rails_6_graphiti_rails.gemfile +1 -1
  16. data/graphiti.gemspec +11 -12
  17. data/lib/graphiti.rb +3 -3
  18. data/lib/graphiti/adapters/abstract.rb +3 -3
  19. data/lib/graphiti/adapters/active_record.rb +65 -36
  20. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +2 -1
  21. data/lib/graphiti/configuration.rb +1 -1
  22. data/lib/graphiti/debugger.rb +4 -4
  23. data/lib/graphiti/delegates/pagination.rb +8 -4
  24. data/lib/graphiti/deserializer.rb +3 -3
  25. data/lib/graphiti/errors.rb +24 -4
  26. data/lib/graphiti/extensions/extra_attribute.rb +1 -1
  27. data/lib/graphiti/query.rb +32 -16
  28. data/lib/graphiti/railtie.rb +1 -1
  29. data/lib/graphiti/request_validator.rb +4 -4
  30. data/lib/graphiti/request_validators/update_validator.rb +1 -2
  31. data/lib/graphiti/request_validators/validator.rb +2 -2
  32. data/lib/graphiti/resource.rb +10 -0
  33. data/lib/graphiti/resource/configuration.rb +10 -3
  34. data/lib/graphiti/resource/dsl.rb +10 -4
  35. data/lib/graphiti/resource/interface.rb +2 -2
  36. data/lib/graphiti/resource/links.rb +3 -3
  37. data/lib/graphiti/resource/persistence.rb +2 -1
  38. data/lib/graphiti/resource/polymorphism.rb +3 -2
  39. data/lib/graphiti/resource/remote.rb +1 -1
  40. data/lib/graphiti/runner.rb +4 -3
  41. data/lib/graphiti/schema.rb +6 -6
  42. data/lib/graphiti/scope.rb +5 -5
  43. data/lib/graphiti/scoping/base.rb +3 -3
  44. data/lib/graphiti/scoping/filter.rb +17 -7
  45. data/lib/graphiti/scoping/sort.rb +1 -1
  46. data/lib/graphiti/serializer.rb +7 -0
  47. data/lib/graphiti/sideload.rb +30 -22
  48. data/lib/graphiti/sideload/belongs_to.rb +1 -1
  49. data/lib/graphiti/sideload/has_many.rb +19 -1
  50. data/lib/graphiti/sideload/many_to_many.rb +6 -2
  51. data/lib/graphiti/stats/payload.rb +4 -4
  52. data/lib/graphiti/types.rb +15 -15
  53. data/lib/graphiti/util/link.rb +6 -2
  54. data/lib/graphiti/util/persistence.rb +16 -10
  55. data/lib/graphiti/util/relationship_payload.rb +4 -4
  56. data/lib/graphiti/util/simple_errors.rb +1 -1
  57. data/lib/graphiti/util/transaction_hooks_recorder.rb +1 -1
  58. data/lib/graphiti/version.rb +1 -1
  59. metadata +18 -22
@@ -11,7 +11,7 @@ module Graphiti
11
11
 
12
12
  # @api private
13
13
  def _all(params, opts, base_scope)
14
- runner = Runner.new(self, params, opts.delete(:query))
14
+ runner = Runner.new(self, params, opts.delete(:query), :all)
15
15
  opts[:params] = params
16
16
  runner.proxy(base_scope, opts)
17
17
  end
@@ -27,7 +27,7 @@ module Graphiti
27
27
  params[:filter] ||= {}
28
28
  params[:filter][:id] = id if id
29
29
 
30
- runner = Runner.new(self, params)
30
+ runner = Runner.new(self, params, nil, :find)
31
31
  runner.proxy base_scope,
32
32
  single: true,
33
33
  raise_on_missing: true,
@@ -39,7 +39,7 @@ module Graphiti
39
39
  path: path,
40
40
  full_path: full_path_for(path),
41
41
  url: url_for(path),
42
- actions: DEFAULT_ACTIONS.dup,
42
+ actions: DEFAULT_ACTIONS.dup
43
43
  }
44
44
  end
45
45
 
@@ -49,7 +49,7 @@ module Graphiti
49
49
  path: path,
50
50
  full_path: full_path_for(path),
51
51
  url: url_for(path),
52
- actions: actions,
52
+ actions: actions
53
53
  }
54
54
  end
55
55
 
@@ -60,7 +60,7 @@ module Graphiti
60
60
  path: path,
61
61
  full_path: full_path_for(path),
62
62
  url: url_for(path),
63
- actions: actions,
63
+ actions: actions
64
64
  }]
65
65
  end
66
66
 
@@ -89,7 +89,8 @@ module Graphiti
89
89
 
90
90
  def update(update_params, meta = nil)
91
91
  model_instance = nil
92
- id = update_params.delete(:id)
92
+ id = update_params[:id]
93
+ update_params = update_params.except(:id)
93
94
 
94
95
  run_callbacks :persistence, :update, update_params, meta do
95
96
  run_callbacks :attributes, :update, update_params, meta do |params|
@@ -58,7 +58,7 @@ module Graphiti
58
58
  end
59
59
 
60
60
  def resource_for_type(type)
61
- resource = children.find { |c| c.type == type }
61
+ resource = children.find { |c| c.type.to_s == type.to_s }
62
62
  if resource.nil?
63
63
  raise Errors::PolymorphicResourceChildNotFound.new(self, type: type)
64
64
  else
@@ -67,7 +67,8 @@ module Graphiti
67
67
  end
68
68
 
69
69
  def resource_for_model(model)
70
- resource = children.find { |c| model.is_a?(c.model) }
70
+ resource = children.find { |c| model.class == c.model } ||
71
+ children.find { |c| model.is_a?(c.model) }
71
72
  if resource.nil?
72
73
  raise Errors::PolymorphicResourceChildNotFound.new(self, model: model)
73
74
  else
@@ -19,7 +19,7 @@ module Graphiti
19
19
  end
20
20
 
21
21
  def save(model, meta)
22
- if meta[:attributes] == {} && meta[:method] == :update
22
+ if meta[:attributes].except(:id) == {} && meta[:method] == :update
23
23
  model
24
24
  else
25
25
  raise Errors::RemoteWrite.new(self.class)
@@ -3,10 +3,11 @@ module Graphiti
3
3
  attr_reader :params
4
4
  attr_reader :deserialized_payload
5
5
 
6
- def initialize(resource_class, params, query = nil)
6
+ def initialize(resource_class, params, query = nil, action = nil)
7
7
  @resource_class = resource_class
8
8
  @params = params
9
9
  @query = query
10
+ @action = action
10
11
 
11
12
  validator = RequestValidator.new(jsonapi_resource, params)
12
13
 
@@ -30,7 +31,7 @@ module Graphiti
30
31
  end
31
32
 
32
33
  def query
33
- @query ||= Query.new(jsonapi_resource, params)
34
+ @query ||= Query.new(jsonapi_resource, params, nil, nil, [], @action)
34
35
  end
35
36
 
36
37
  def query_hash
@@ -50,7 +51,7 @@ module Graphiti
50
51
  def jsonapi_render_options
51
52
  options = {}
52
53
  options.merge!(default_jsonapi_render_options)
53
- options[:meta] ||= {}
54
+ options[:meta] ||= {}
54
55
  options[:expose] ||= {}
55
56
  options[:expose][:context] = jsonapi_context
56
57
  options
@@ -33,7 +33,7 @@ module Graphiti
33
33
  {
34
34
  resources: generate_resources,
35
35
  endpoints: generate_endpoints,
36
- types: generate_types,
36
+ types: generate_types
37
37
  }
38
38
  end
39
39
 
@@ -94,7 +94,7 @@ module Graphiti
94
94
  extra_attributes: extra_attributes(r),
95
95
  sorts: sorts(r),
96
96
  filters: filters(r),
97
- relationships: relationships(r),
97
+ relationships: relationships(r)
98
98
  }
99
99
 
100
100
  if r.default_sort
@@ -121,7 +121,7 @@ module Graphiti
121
121
  name: r.name,
122
122
  description: r.description,
123
123
  remote: r.remote_url,
124
- relationships: relationships(r),
124
+ relationships: relationships(r)
125
125
  }
126
126
  }
127
127
 
@@ -136,7 +136,7 @@ module Graphiti
136
136
  type: config[:type].to_s,
137
137
  readable: flag(config[:readable]),
138
138
  writable: flag(config[:writable]),
139
- description: resource.attribute_description(name),
139
+ description: resource.attribute_description(name)
140
140
  }
141
141
  end
142
142
  end
@@ -149,7 +149,7 @@ module Graphiti
149
149
  attrs[name] = {
150
150
  type: config[:type].to_s,
151
151
  readable: flag(config[:readable]),
152
- description: resource.attribute_description(name),
152
+ description: resource.attribute_description(name)
153
153
  }
154
154
  end
155
155
  end
@@ -186,7 +186,7 @@ module Graphiti
186
186
 
187
187
  config = {
188
188
  type: filter[:type].to_s,
189
- operators: filter[:operators].keys.map(&:to_s),
189
+ operators: filter[:operators].keys.map(&:to_s)
190
190
  }
191
191
 
192
192
  config[:single] = true if filter[:single]
@@ -3,10 +3,10 @@ module Graphiti
3
3
  attr_accessor :object, :unpaginated_object
4
4
  attr_reader :pagination
5
5
  def initialize(object, resource, query, opts = {})
6
- @object = object
7
- @resource = resource
8
- @query = query
9
- @opts = opts
6
+ @object = object
7
+ @resource = resource
8
+ @query = query
9
+ @opts = opts
10
10
 
11
11
  @object = @resource.around_scoping(@object, @query.hash) { |scope|
12
12
  apply_scoping(scope, opts)
@@ -75,7 +75,7 @@ module Graphiti
75
75
  resource: @resource,
76
76
  params: @opts[:params],
77
77
  sideload: @opts[:sideload],
78
- parent: @opts[:parent],
78
+ parent: @opts[:parent]
79
79
  # Set once data is resolved within block
80
80
  # results: ...
81
81
  }
@@ -25,9 +25,9 @@ module Graphiti
25
25
  # @param [Hash] opts configuration options used by subclasses
26
26
  def initialize(resource, query_hash, scope, opts = {})
27
27
  @query_hash = query_hash
28
- @resource = resource
29
- @scope = scope
30
- @opts = opts
28
+ @resource = resource
29
+ @scope = scope
30
+ @opts = opts
31
31
  end
32
32
 
33
33
  # Apply this scoping criteria.
@@ -31,7 +31,7 @@ module Graphiti
31
31
 
32
32
  def filter_via_adapter(filter, operator, value)
33
33
  type_name = Types.name_for(filter.values.first[:type])
34
- method = :"filter_#{type_name}_#{operator}"
34
+ method = :"filter_#{type_name}_#{operator}"
35
35
  attribute = filter.keys.first
36
36
 
37
37
  if resource.adapter.respond_to?(method)
@@ -54,6 +54,8 @@ module Graphiti
54
54
  unless type[:canonical_name] == :hash || !value.is_a?(String)
55
55
  value = parse_string_value(filter.values[0], value)
56
56
  end
57
+
58
+ check_deny_empty_filters!(resource, filter, value)
57
59
  value = parse_string_null(filter.values[0], value)
58
60
  validate_singular(resource, filter, value)
59
61
  value = coerce_types(filter.values[0], param_name.to_sym, value)
@@ -82,11 +84,11 @@ module Graphiti
82
84
  type = Types[filter.values[0][:type]][:canonical_name]
83
85
  if param_value.is_a?(Hash) && type == :hash
84
86
  operators_keys = filter.values[0][:operators].keys
85
- unless param_value.keys.all? {|k| operators_keys.include?(k)}
86
- param_value = { eq: param_value }
87
+ unless param_value.keys.all? { |k| operators_keys.include?(k) }
88
+ param_value = {eq: param_value}
87
89
  end
88
90
  elsif !param_value.is_a?(Hash) || param_value.empty?
89
- param_value = { eq: param_value }
91
+ param_value = {eq: param_value}
90
92
  end
91
93
 
92
94
  param_value.map do |operator, value|
@@ -184,10 +186,10 @@ module Graphiti
184
186
  # remove the quote characters from the quoted strings
185
187
  quotes.each { |q| q.gsub!("{{", "").gsub!("}}", "") }
186
188
  # merge everything back together into an array
187
- if singular_filter
188
- value = Array(value) + quotes
189
+ value = if singular_filter
190
+ Array(value) + quotes
189
191
  else
190
- value = Array(value.split(",")) + quotes
192
+ Array(value.split(",")) + quotes
191
193
  end
192
194
  # remove any blanks that are left
193
195
  value.reject! { |v| v.length.zero? }
@@ -200,5 +202,13 @@ module Graphiti
200
202
 
201
203
  value
202
204
  end
205
+
206
+ def check_deny_empty_filters!(resource, filter, value)
207
+ return unless filter.values[0][:deny_empty]
208
+
209
+ if value.nil? || value.empty? || value == "null"
210
+ raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
211
+ end
212
+ end
203
213
  end
204
214
  end
@@ -78,7 +78,7 @@ module Graphiti
78
78
 
79
79
  def sort_hash(attr)
80
80
  value = attr[0] == "-" ? :desc : :asc
81
- key = attr.sub("-", "").to_sym
81
+ key = attr.sub("-", "").to_sym
82
82
 
83
83
  {key => value}
84
84
  end
@@ -25,6 +25,7 @@ module Graphiti
25
25
  def as_jsonapi(*)
26
26
  super.tap do |hash|
27
27
  strip_relationships!(hash) if strip_relationships?
28
+ add_links!(hash)
28
29
  end
29
30
  end
30
31
 
@@ -49,6 +50,12 @@ module Graphiti
49
50
 
50
51
  private
51
52
 
53
+ def add_links!(hash)
54
+ return unless @resource.respond_to?(:links?)
55
+
56
+ hash[:links] = @resource.links(@object) if @resource.links?
57
+ end
58
+
52
59
  def strip_relationships!(hash)
53
60
  hash[:relationships]&.select! do |name, payload|
54
61
  payload.key?(:data)
@@ -18,31 +18,31 @@ module Graphiti
18
18
  :link_proc
19
19
 
20
20
  def initialize(name, opts)
21
- @name = name
21
+ @name = name
22
22
  validate_options!(opts)
23
- @parent_resource_class = opts[:parent_resource]
24
- @resource_class = opts[:resource]
25
- @primary_key = opts[:primary_key]
26
- @foreign_key = opts[:foreign_key]
27
- @type = opts[:type]
28
- @base_scope = opts[:base_scope]
29
- @readable = evaluate_flag(opts[:readable])
30
- @writable = evaluate_flag(opts[:writable])
31
- @as = opts[:as]
32
- @link = opts[:link]
33
- @single = opts[:single]
34
- @remote = opts[:remote]
23
+ @parent_resource_class = opts[:parent_resource]
24
+ @resource_class = opts[:resource]
25
+ @primary_key = opts[:primary_key]
26
+ @foreign_key = opts[:foreign_key]
27
+ @type = opts[:type]
28
+ @base_scope = opts[:base_scope]
29
+ @readable = evaluate_flag(opts[:readable])
30
+ @writable = evaluate_flag(opts[:writable])
31
+ @as = opts[:as]
32
+ @link = opts[:link]
33
+ @single = opts[:single]
34
+ @remote = opts[:remote]
35
35
  apply_belongs_to_many_filter if type == :many_to_many
36
36
 
37
- @description = opts[:description]
37
+ @description = opts[:description]
38
38
 
39
39
  # polymorphic has_many
40
- @polymorphic_as = opts[:polymorphic_as]
40
+ @polymorphic_as = opts[:polymorphic_as]
41
41
  # polymorphic_belongs_to-specific
42
- @group_name = opts[:group_name]
43
- @polymorphic_child = opts[:polymorphic_child]
44
- @parent = opts[:parent]
45
- @always_include_resource_ids = opts[:always_include_resource_ids]
42
+ @group_name = opts[:group_name]
43
+ @polymorphic_child = opts[:polymorphic_child]
44
+ @parent = opts[:parent]
45
+ @always_include_resource_ids = opts[:always_include_resource_ids]
46
46
 
47
47
  if polymorphic_child?
48
48
  parent.resource.polymorphic << resource_class
@@ -132,6 +132,10 @@ module Graphiti
132
132
  end
133
133
  end
134
134
 
135
+ def link_filter(parents)
136
+ base_filter(parents)
137
+ end
138
+
135
139
  # The parent resource is a remote,
136
140
  # AND the sideload is a remote to the same endpoint
137
141
  def shared_remote?
@@ -197,7 +201,7 @@ module Graphiti
197
201
 
198
202
  with_error_handling Errors::SideloadParamsError do
199
203
  params = load_params(parents, query)
200
- params_proc&.call(params, parents)
204
+ params_proc&.call(params, parents, context)
201
205
  return [] if blank_query?(params)
202
206
  opts = load_options(parents, query)
203
207
  opts[:sideload] = self
@@ -426,13 +430,17 @@ module Graphiti
426
430
  return false if flag.blank?
427
431
 
428
432
  case flag.class.name
429
- when "Symbol","String"
433
+ when "Symbol", "String"
430
434
  resource.send(flag)
431
435
  when "Proc"
432
- self.resource.instance_exec(&flag)
436
+ resource.instance_exec(&flag)
433
437
  else
434
438
  !!flag
435
439
  end
436
440
  end
441
+
442
+ def context
443
+ Graphiti.context[:object]
444
+ end
437
445
  end
438
446
  end
@@ -1,6 +1,6 @@
1
1
  class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
2
2
  def initialize(name, opts)
3
- opts = { always_include_resource_ids: false }.merge(opts)
3
+ opts = {always_include_resource_ids: false}.merge(opts)
4
4
  super(name, opts)
5
5
  end
6
6
 
@@ -1,8 +1,18 @@
1
1
  class Graphiti::Sideload::HasMany < Graphiti::Sideload
2
+ def initialize(name, opts)
3
+ @inverse_filter = opts[:inverse_filter]
4
+
5
+ super(name, opts)
6
+ end
7
+
2
8
  def type
3
9
  :has_many
4
10
  end
5
11
 
12
+ def inverse_filter
13
+ @inverse_filter || foreign_key
14
+ end
15
+
6
16
  def load_params(parents, query)
7
17
  query.hash.tap do |hash|
8
18
  hash[:filter] ||= {}
@@ -11,11 +21,19 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
11
21
  end
12
22
 
13
23
  def base_filter(parents)
14
- {foreign_key => ids_for_parents(parents).join(",")}
24
+ {foreign_key => parent_filter(parents)}
25
+ end
26
+
27
+ def link_filter(parents)
28
+ {inverse_filter => parent_filter(parents)}
15
29
  end
16
30
 
17
31
  private
18
32
 
33
+ def parent_filter(parents)
34
+ ids_for_parents(parents).join(",")
35
+ end
36
+
19
37
  def child_map(children)
20
38
  children.group_by(&foreign_key)
21
39
  end
@@ -11,8 +11,12 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
11
11
  foreign_key.values.first
12
12
  end
13
13
 
14
+ def inverse_filter
15
+ @inverse_filter || true_foreign_key
16
+ end
17
+
14
18
  def base_filter(parents)
15
- {true_foreign_key => ids_for_parents(parents).join(",")}
19
+ {true_foreign_key => parent_filter(parents)}
16
20
  end
17
21
 
18
22
  def infer_foreign_key
@@ -32,7 +36,7 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
32
36
  self_ref = self
33
37
  fk_type = parent_resource_class.attributes[:id][:type]
34
38
  fk_type = :hash if polymorphic?
35
- resource_class.filter true_foreign_key, fk_type do
39
+ resource_class.filter inverse_filter, fk_type do
36
40
  eq do |scope, value|
37
41
  self_ref.belongs_to_many_filter(scope, value)
38
42
  end