graphiti 1.2.16 → 1.2.21

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 +12 -8
  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 +2 -1
  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
@@ -36,7 +36,8 @@ class Graphiti::Adapters::ActiveRecord::ManyToManySideload < Graphiti::Sideload:
36
36
 
37
37
  def filter_for(scope, value, type = nil)
38
38
  scope
39
- .includes(through_relationship_name)
39
+ .preload(through_relationship_name)
40
+ .joins(through_relationship_name)
40
41
  .where(belongs_to_many_clause(value, type))
41
42
  end
42
43
 
@@ -43,7 +43,7 @@ module Graphiti
43
43
  end
44
44
 
45
45
  if (logger = ::Rails.logger)
46
- self.debug = logger.level.zero? && self.debug
46
+ self.debug = logger.level.zero? && debug
47
47
  Graphiti.logger = logger
48
48
  end
49
49
  end
@@ -10,6 +10,8 @@ module Graphiti
10
10
 
11
11
  class << self
12
12
  def on_data(name, start, stop, id, payload)
13
+ return [] unless enabled
14
+
13
15
  took = ((stop - start) * 1000.0).round(2)
14
16
  params = scrub_params(payload[:params])
15
17
 
@@ -24,7 +26,7 @@ module Graphiti
24
26
  end
25
27
  end
26
28
 
27
- def on_data_exception(payload, params)
29
+ private def on_data_exception(payload, params)
28
30
  unless payload[:exception_object].instance_variable_get(:@__graphiti_debug)
29
31
  add_chunk do |logs, json|
30
32
  logs << ["\n=== Graphiti Debug ERROR", :red, true]
@@ -49,20 +51,20 @@ module Graphiti
49
51
  end
50
52
  end
51
53
 
52
- def results(raw_results)
54
+ private def results(raw_results)
53
55
  raw_results.map { |r| "[#{r.class.name}, #{r.id.inspect}]" }.join(", ")
54
56
  end
55
57
 
56
- def on_sideload_data(payload, params, took)
58
+ private def on_sideload_data(payload, params, took)
57
59
  sideload = payload[:sideload]
58
60
  results = results(payload[:results])
59
61
  add_chunk(payload[:resource], payload[:parent]) do |logs, json|
60
62
  logs << [" \\_ #{sideload.name}", :yellow, true]
61
63
  json[:name] = sideload.name
62
- if sideload.class.scope_proc
63
- query = "#{payload[:resource].class.name}: Manual sideload via .scope"
64
+ query = if sideload.class.scope_proc
65
+ "#{payload[:resource].class.name}: Manual sideload via .scope"
64
66
  else
65
- query = "#{payload[:resource].class.name}.all(#{params.inspect})"
67
+ "#{payload[:resource].class.name}.all(#{params.inspect})"
66
68
  end
67
69
  logs << [" #{query}", :cyan, true]
68
70
  json[:query] = query
@@ -72,7 +74,7 @@ module Graphiti
72
74
  end
73
75
  end
74
76
 
75
- def on_primary_data(payload, params, took)
77
+ private def on_primary_data(payload, params, took)
76
78
  results = results(payload[:results])
77
79
  add_chunk(payload[:resource], payload[:parent]) do |logs, json|
78
80
  logs << [""]
@@ -90,6 +92,8 @@ module Graphiti
90
92
  end
91
93
 
92
94
  def on_render(name, start, stop, id, payload)
95
+ return [] unless enabled
96
+
93
97
  add_chunk do |logs|
94
98
  took = ((stop - start) * 1000.0).round(2)
95
99
  logs << [""]
@@ -148,7 +152,7 @@ module Graphiti
148
152
  parent: parent,
149
153
  logs: logs,
150
154
  json: json,
151
- children: [],
155
+ children: []
152
156
  }
153
157
  end
154
158
 
@@ -15,22 +15,26 @@ module Graphiti
15
15
  links[:last] = pagination_link(last_page)
16
16
  links[:prev] = pagination_link(current_page - 1) unless current_page == 1
17
17
  links[:next] = pagination_link(current_page + 1) unless current_page == last_page
18
- end.select {|k, v| !v.nil? }
18
+ end.select { |k, v| !v.nil? }
19
19
  end
20
20
 
21
21
  private
22
22
 
23
+ def pagination_params
24
+ @pagination_params ||= @proxy.query.params.reject { |key, _| [:action, :controller, :format].include?(key) }
25
+ end
26
+
23
27
  def pagination_link(page)
24
28
  return nil unless @proxy.resource.endpoint
25
29
 
26
30
  uri = URI(@proxy.resource.endpoint[:url].to_s)
27
31
 
28
32
  # Overwrite the pagination query params with the desired page
29
- uri.query = @proxy.query.hash.merge({
33
+ uri.query = pagination_params.merge({
30
34
  page: {
31
35
  number: page,
32
- size: page_size,
33
- },
36
+ size: page_size
37
+ }
34
38
  }).to_query
35
39
  uri.to_s
36
40
  end
@@ -87,7 +87,7 @@ class Graphiti::Deserializer
87
87
  type: data[:type],
88
88
  temp_id: data[:'temp-id'],
89
89
  method: action,
90
- payload_path: ["data"],
90
+ payload_path: ["data"]
91
91
  }
92
92
  end
93
93
 
@@ -185,10 +185,10 @@ class Graphiti::Deserializer
185
185
  jsonapi_type: datum[:type],
186
186
  temp_id: temp_id,
187
187
  method: method,
188
- payload_path: ["included", included_idx],
188
+ payload_path: ["included", included_idx]
189
189
  },
190
190
  attributes: attributes,
191
- relationships: relationships,
191
+ relationships: relationships
192
192
  }
193
193
  end
194
194
 
@@ -143,8 +143,13 @@ module Graphiti
143
143
  def message
144
144
  allow = @filter.values[0][:allow]
145
145
  deny = @filter.values[0][:deny]
146
+ value_string = if @value == "(empty)"
147
+ "empty value"
148
+ else
149
+ "value #{@value.inspect}"
150
+ end
146
151
  msg = <<-MSG
147
- #{@resource.class.name}: tried to filter on #{@filter.keys[0].inspect}, but passed invalid value #{@value.inspect}.
152
+ #{@resource.class.name}: tried to filter on #{@filter.keys[0].inspect}, but passed invalid #{value_string}.
148
153
  MSG
149
154
  msg << "\nAllowlist: #{allow.inspect}" if allow
150
155
  msg << "\nDenylist: #{deny.inspect}" if deny
@@ -189,7 +194,7 @@ module Graphiti
189
194
 
190
195
  Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
191
196
 
192
- If you do not wish to generate a link, pass link: false or set self.relationship_links_by_default = false.
197
+ If you do not wish to generate a link, pass link: false or set self.autolink = false.
193
198
  MSG
194
199
  end
195
200
  end
@@ -316,14 +321,14 @@ module Graphiti
316
321
  sortable: "sort on",
317
322
  filterable: "filter on",
318
323
  readable: "read",
319
- writable: "write",
324
+ writable: "write"
320
325
  }[@flag]
321
326
  else
322
327
  {
323
328
  sortable: "add sort",
324
329
  filterable: "add filter",
325
330
  readable: "read",
326
- writable: "write",
331
+ writable: "write"
327
332
  }[@flag]
328
333
  end
329
334
  end
@@ -722,6 +727,21 @@ module Graphiti
722
727
  end
723
728
 
724
729
  class RecordNotFound < Base
730
+ def initialize(resource = nil, id = nil, path = nil)
731
+ @resource = resource
732
+ @id = id
733
+ @path = path
734
+ end
735
+
736
+ def message
737
+ if !@resource.nil? && !@id.nil?
738
+ "The referenced resource '#{@resource}' with id '#{@id}' could not be found.".tap do |msg|
739
+ msg << " Referenced at '#{@path}'" unless @path.nil?
740
+ end
741
+ else
742
+ "Specified Record Not Found"
743
+ end
744
+ end
725
745
  end
726
746
 
727
747
  class RequiredFilter < Base
@@ -43,7 +43,7 @@ module Graphiti
43
43
  def extra_attribute(name, options = {}, &blk)
44
44
  allow_field = proc {
45
45
  if options[:if]
46
- next false unless instance_eval(&options[:if])
46
+ next false unless instance_exec(&options[:if])
47
47
  end
48
48
 
49
49
  @extra_fields &&
@@ -1,8 +1,8 @@
1
1
  module Graphiti
2
2
  class Query
3
- attr_reader :resource, :association_name, :params
3
+ attr_reader :resource, :association_name, :params, :action
4
4
 
5
- def initialize(resource, params, association_name = nil, nested_include = nil, parents = [])
5
+ def initialize(resource, params, association_name = nil, nested_include = nil, parents = [], action = nil)
6
6
  @resource = resource
7
7
  @association_name = association_name
8
8
  @params = params
@@ -11,6 +11,7 @@ module Graphiti
11
11
  @params = @params.deep_symbolize_keys
12
12
  @include_param = nested_include || @params[:include]
13
13
  @parents = parents
14
+ @action = parse_action(action)
14
15
  end
15
16
 
16
17
  def association?
@@ -44,16 +45,19 @@ module Graphiti
44
45
 
45
46
  def hash
46
47
  @hash ||= {}.tap do |h|
47
- h[:filter] = filters unless filters.empty?
48
- h[:sort] = sorts unless sorts.empty?
49
- h[:page] = pagination unless pagination.empty?
50
- unless association?
51
- h[:fields] = fields unless fields.empty?
52
- h[:extra_fields] = extra_fields unless extra_fields.empty?
48
+ h[:filter] = filters
49
+ h[:sort] = sorts
50
+ h[:page] = pagination
51
+ if association?
52
+ resource_type = @resource.class.type
53
+ h[:extra_fields] = {resource_type => extra_fields[resource_type]} if extra_fields.key?(resource_type)
54
+ else
55
+ h[:fields] = fields
56
+ h[:extra_fields] = extra_fields
53
57
  end
54
- h[:stats] = stats unless stats.empty?
55
- h[:include] = sideload_hash unless sideload_hash.empty?
56
- end
58
+ h[:stats] = stats
59
+ h[:include] = sideload_hash
60
+ end.reject { |_, value| value.empty? }
57
61
  end
58
62
 
59
63
  def zero_results?
@@ -92,7 +96,7 @@ module Graphiti
92
96
  sl_resource = resource_for_sideload(sideload)
93
97
  query_parents = parents + [self]
94
98
  sub_hash = sub_hash[:include] if sub_hash.key?(:include)
95
- hash[key] = Query.new(sl_resource, @params, key, sub_hash, query_parents)
99
+ hash[key] = Query.new(sl_resource, @params, key, sub_hash, query_parents, :all)
96
100
  else
97
101
  handle_missing_sideload(key)
98
102
  end
@@ -232,7 +236,7 @@ module Graphiti
232
236
  return true if @resource.remote?
233
237
 
234
238
  if (att = @resource.get_attr(name, flag, request: true))
235
- return att
239
+ att
236
240
  else
237
241
  not_associated_name = !@resource.class.association_names.include?(name)
238
242
  not_associated_type = !@resource.class.association_types.include?(name)
@@ -262,8 +266,8 @@ module Graphiti
262
266
  def parse_fieldset(fieldset)
263
267
  {}.tap do |hash|
264
268
  fieldset.each_pair do |type, fields|
265
- type = type.to_sym
266
- fields = fields.split(",") unless fields.is_a?(Array)
269
+ type = type.to_sym
270
+ fields = fields.split(",") unless fields.is_a?(Array)
267
271
  hash[type] = fields.map(&:to_sym)
268
272
  end
269
273
  end
@@ -282,7 +286,7 @@ module Graphiti
282
286
 
283
287
  def sort_hash(attr)
284
288
  value = attr[0] == "-" ? :desc : :asc
285
- key = attr.sub("-", "").to_sym
289
+ key = attr.sub("-", "").to_sym
286
290
 
287
291
  {key => value}
288
292
  end
@@ -312,5 +316,17 @@ module Graphiti
312
316
  end
313
317
  end
314
318
  end
319
+
320
+ def parse_action(action)
321
+ action ||= @params.fetch(:action, Graphiti.context[:namespace]).try(:to_sym)
322
+ case action
323
+ when :index
324
+ :all
325
+ when :show
326
+ :find
327
+ else
328
+ action
329
+ end
330
+ end
315
331
  end
316
332
  end
@@ -6,7 +6,7 @@ module Graphiti
6
6
  end
7
7
 
8
8
  generators do
9
- Dir[File.expand_path("../../deprecated_generators/**/*.rb", __dir__)].each do |f|
9
+ Dir[File.expand_path("../../deprecated_generators/**/*.rb", __dir__)].sort.each do |f|
10
10
  require f
11
11
  end
12
12
  end
@@ -1,10 +1,10 @@
1
1
  module Graphiti
2
2
  class RequestValidator
3
3
  delegate :validate,
4
- :validate!,
5
- :errors,
6
- :deserialized_payload,
7
- to: :@validator
4
+ :validate!,
5
+ :errors,
6
+ :deserialized_payload,
7
+ to: :@validator
8
8
 
9
9
  def initialize(root_resource, raw_params)
10
10
  @validator = ValidatorFactory.create(root_resource, raw_params)
@@ -5,7 +5,7 @@ module Graphiti
5
5
  if required_payload? && payload_matches_endpoint?
6
6
  super
7
7
  else
8
- return false
8
+ false
9
9
  end
10
10
  end
11
11
 
@@ -36,7 +36,6 @@ module Graphiti
36
36
  attribute_mismatch([:data, :id])
37
37
  end
38
38
 
39
-
40
39
  meta_type = @raw_params.dig(:data, :type)
41
40
 
42
41
  # NOTE: calling #to_s and comparing 2 strings is slower than
@@ -25,7 +25,7 @@ module Graphiti
25
25
 
26
26
  def validate!
27
27
  unless validate
28
- raise @error_class || Graphiti::Errors::InvalidRequest, self.errors
28
+ raise @error_class || Graphiti::Errors::InvalidRequest, errors
29
29
  end
30
30
 
31
31
  true
@@ -47,7 +47,7 @@ module Graphiti
47
47
  def process_relationships(resource, relationships, payload_path)
48
48
  opts = {
49
49
  resource: resource,
50
- relationships: relationships,
50
+ relationships: relationships
51
51
  }
52
52
 
53
53
  Graphiti::Util::RelationshipPayload.iterate(opts) do |x|
@@ -148,5 +148,15 @@ module Graphiti
148
148
  end
149
149
  response
150
150
  end
151
+
152
+ def links?
153
+ self.class.links.any?
154
+ end
155
+
156
+ def links(model)
157
+ self.class.links.each_with_object({}) do |(name, blk), memo|
158
+ memo[name] = instance_exec(model, &blk)
159
+ end
160
+ end
151
161
  end
152
162
  end
@@ -22,7 +22,7 @@ module Graphiti
22
22
  end
23
23
 
24
24
  def type=(val)
25
- val = val && val.to_sym
25
+ val = val&.to_sym
26
26
  if (val = super)
27
27
  serializer.type(val)
28
28
  end
@@ -42,7 +42,7 @@ module Graphiti
42
42
  path: val,
43
43
  full_path: val,
44
44
  url: val,
45
- actions: [:index, :show],
45
+ actions: [:index, :show]
46
46
  }
47
47
  end
48
48
 
@@ -82,7 +82,8 @@ module Graphiti
82
82
  :attributes_schema_by_default,
83
83
  :relationships_readable_by_default,
84
84
  :relationships_writable_by_default,
85
- :filters_accept_nil_by_default
85
+ :filters_accept_nil_by_default,
86
+ :filters_deny_empty_by_default
86
87
 
87
88
  class << self
88
89
  prepend Overrides
@@ -104,6 +105,7 @@ module Graphiti
104
105
  default(klass, :relationships_readable_by_default, true)
105
106
  default(klass, :relationships_writable_by_default, true)
106
107
  default(klass, :filters_accept_nil_by_default, false)
108
+ default(klass, :filters_deny_empty_by_default, false)
107
109
 
108
110
  unless klass.config[:attributes][:id]
109
111
  klass.attribute :id, :integer_id
@@ -198,6 +200,7 @@ module Graphiti
198
200
  extra_attributes: {},
199
201
  sideloads: {},
200
202
  callbacks: {},
203
+ links: {}
201
204
  }
202
205
  end
203
206
 
@@ -236,6 +239,10 @@ module Graphiti
236
239
  def default_filters
237
240
  config[:default_filters]
238
241
  end
242
+
243
+ def links
244
+ config[:links]
245
+ end
239
246
  end
240
247
 
241
248
  def get_attr!(name, flag, options = {})
@@ -32,7 +32,8 @@ module Graphiti
32
32
  dependencies: opts[:dependent],
33
33
  required: required,
34
34
  operators: operators.to_hash,
35
- allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default)
35
+ allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default),
36
+ deny_empty: opts.fetch(:deny_empty, filters_deny_empty_by_default)
36
37
  }
37
38
  elsif (type = args[0])
38
39
  attribute name, type, only: [:filterable], allow: opts[:allow]
@@ -55,7 +56,7 @@ module Graphiti
55
56
 
56
57
  if get_attr(name, :sortable, raise_error: :only_unsupported)
57
58
  config[:sorts][name] = {
58
- proc: blk,
59
+ proc: blk
59
60
  }.merge(opts.slice(:only))
60
61
  elsif (type = args[0])
61
62
  attribute name, type, only: [:sortable]
@@ -78,7 +79,7 @@ module Graphiti
78
79
  def default_filter(name = nil, &blk)
79
80
  name ||= :__default
80
81
  config[:default_filters][name.to_sym] = {
81
- filter: blk,
82
+ filter: blk
82
83
  }
83
84
  end
84
85
 
@@ -131,9 +132,10 @@ module Graphiti
131
132
  readable: true,
132
133
  writable: false,
133
134
  sortable: false,
134
- filterable: false,
135
+ filterable: false
135
136
  }
136
137
  options = defaults.merge(options)
138
+ attribute_option(options, :readable)
137
139
  config[:extra_attributes][name] = options
138
140
  apply_extra_attributes_to_serializer
139
141
  end
@@ -146,6 +148,10 @@ module Graphiti
146
148
  end
147
149
  end
148
150
 
151
+ def link(name, &blk)
152
+ config[:links][name.to_sym] = blk
153
+ end
154
+
149
155
  def all_attributes
150
156
  attributes.merge(extra_attributes)
151
157
  end