graphiti 1.2.16 → 1.3.9

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.standard.yml +4 -4
  4. data/Appraisals +23 -17
  5. data/CHANGELOG.md +7 -1
  6. data/Guardfile +5 -5
  7. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  8. data/deprecated_generators/graphiti/resource_generator.rb +1 -1
  9. data/gemfiles/{rails_5.gemfile → rails_5_2.gemfile} +2 -2
  10. data/gemfiles/{rails_5_graphiti_rails.gemfile → rails_5_2_graphiti_rails.gemfile} +3 -4
  11. data/gemfiles/rails_6.gemfile +1 -1
  12. data/gemfiles/rails_6_graphiti_rails.gemfile +2 -3
  13. data/gemfiles/{rails_4.gemfile → rails_7.gemfile} +2 -2
  14. data/gemfiles/rails_7_graphiti_rails.gemfile +19 -0
  15. data/graphiti.gemspec +16 -16
  16. data/lib/graphiti/adapters/abstract.rb +20 -5
  17. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +1 -1
  18. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +1 -1
  19. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +1 -1
  20. data/lib/graphiti/adapters/active_record/{inferrence.rb → inference.rb} +2 -2
  21. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +19 -0
  22. data/lib/graphiti/adapters/active_record.rb +119 -74
  23. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  24. data/lib/graphiti/adapters/null.rb +1 -1
  25. data/lib/graphiti/adapters/persistence/associations.rb +78 -0
  26. data/lib/graphiti/configuration.rb +3 -1
  27. data/lib/graphiti/debugger.rb +12 -8
  28. data/lib/graphiti/delegates/pagination.rb +47 -13
  29. data/lib/graphiti/deserializer.rb +3 -3
  30. data/lib/graphiti/errors.rb +109 -15
  31. data/lib/graphiti/extensions/extra_attribute.rb +4 -4
  32. data/lib/graphiti/extensions/temp_id.rb +1 -1
  33. data/lib/graphiti/filter_operators.rb +0 -1
  34. data/lib/graphiti/hash_renderer.rb +198 -21
  35. data/lib/graphiti/query.rb +105 -73
  36. data/lib/graphiti/railtie.rb +5 -5
  37. data/lib/graphiti/renderer.rb +19 -1
  38. data/lib/graphiti/request_validator.rb +10 -10
  39. data/lib/graphiti/request_validators/update_validator.rb +4 -5
  40. data/lib/graphiti/request_validators/validator.rb +38 -24
  41. data/lib/graphiti/resource/configuration.rb +35 -7
  42. data/lib/graphiti/resource/dsl.rb +34 -8
  43. data/lib/graphiti/resource/interface.rb +13 -3
  44. data/lib/graphiti/resource/links.rb +3 -3
  45. data/lib/graphiti/resource/persistence.rb +2 -1
  46. data/lib/graphiti/resource/polymorphism.rb +8 -2
  47. data/lib/graphiti/resource/remote.rb +2 -2
  48. data/lib/graphiti/resource/sideloading.rb +4 -4
  49. data/lib/graphiti/resource.rb +12 -1
  50. data/lib/graphiti/resource_proxy.rb +23 -3
  51. data/lib/graphiti/runner.rb +5 -5
  52. data/lib/graphiti/schema.rb +36 -11
  53. data/lib/graphiti/schema_diff.rb +44 -4
  54. data/lib/graphiti/scope.rb +8 -10
  55. data/lib/graphiti/scoping/base.rb +3 -3
  56. data/lib/graphiti/scoping/filter.rb +36 -15
  57. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  58. data/lib/graphiti/scoping/paginate.rb +47 -3
  59. data/lib/graphiti/scoping/sort.rb +5 -7
  60. data/lib/graphiti/serializer.rb +49 -7
  61. data/lib/graphiti/sideload/belongs_to.rb +1 -1
  62. data/lib/graphiti/sideload/has_many.rb +19 -1
  63. data/lib/graphiti/sideload/many_to_many.rb +11 -4
  64. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -4
  65. data/lib/graphiti/sideload.rb +47 -23
  66. data/lib/graphiti/stats/dsl.rb +0 -1
  67. data/lib/graphiti/stats/payload.rb +12 -9
  68. data/lib/graphiti/types.rb +15 -15
  69. data/lib/graphiti/util/attribute_check.rb +1 -1
  70. data/lib/graphiti/util/class.rb +6 -0
  71. data/lib/graphiti/util/link.rb +10 -2
  72. data/lib/graphiti/util/persistence.rb +21 -78
  73. data/lib/graphiti/util/relationship_payload.rb +4 -4
  74. data/lib/graphiti/util/remote_params.rb +9 -4
  75. data/lib/graphiti/util/remote_serializer.rb +1 -0
  76. data/lib/graphiti/util/serializer_attributes.rb +41 -11
  77. data/lib/graphiti/util/simple_errors.rb +4 -4
  78. data/lib/graphiti/util/transaction_hooks_recorder.rb +1 -1
  79. data/lib/graphiti/version.rb +1 -1
  80. data/lib/graphiti.rb +6 -3
  81. metadata +46 -37
  82. data/.travis.yml +0 -59
@@ -1,7 +1,7 @@
1
1
  module Graphiti
2
2
  module Adapters
3
3
  class ActiveRecord < ::Graphiti::Adapters::Abstract
4
- require "graphiti/adapters/active_record/inferrence"
4
+ require "graphiti/adapters/active_record/inference"
5
5
  require "graphiti/adapters/active_record/has_many_sideload"
6
6
  require "graphiti/adapters/active_record/belongs_to_sideload"
7
7
  require "graphiti/adapters/active_record/has_one_sideload"
@@ -12,31 +12,31 @@ module Graphiti
12
12
  has_many: Graphiti::Adapters::ActiveRecord::HasManySideload,
13
13
  has_one: Graphiti::Adapters::ActiveRecord::HasOneSideload,
14
14
  belongs_to: Graphiti::Adapters::ActiveRecord::BelongsToSideload,
15
- many_to_many: Graphiti::Adapters::ActiveRecord::ManyToManySideload,
15
+ many_to_many: Graphiti::Adapters::ActiveRecord::ManyToManySideload
16
16
  }
17
17
  end
18
18
 
19
19
  def filter_eq(scope, attribute, value)
20
20
  scope.where(attribute => value)
21
21
  end
22
- alias filter_integer_eq filter_eq
23
- alias filter_float_eq filter_eq
24
- alias filter_big_decimal_eq filter_eq
25
- alias filter_date_eq filter_eq
26
- alias filter_boolean_eq filter_eq
27
- alias filter_uuid_eq filter_eq
28
- alias filter_enum_eq filter_eq
22
+ alias_method :filter_integer_eq, :filter_eq
23
+ alias_method :filter_float_eq, :filter_eq
24
+ alias_method :filter_big_decimal_eq, :filter_eq
25
+ alias_method :filter_date_eq, :filter_eq
26
+ alias_method :filter_boolean_eq, :filter_eq
27
+ alias_method :filter_uuid_eq, :filter_eq
28
+ alias_method :filter_enum_eq, :filter_eq
29
29
 
30
30
  def filter_not_eq(scope, attribute, value)
31
31
  scope.where.not(attribute => value)
32
32
  end
33
- alias filter_integer_not_eq filter_not_eq
34
- alias filter_float_not_eq filter_not_eq
35
- alias filter_big_decimal_not_eq filter_not_eq
36
- alias filter_date_not_eq filter_not_eq
37
- alias filter_boolean_not_eq filter_not_eq
38
- alias filter_uuid_not_eq filter_not_eq
39
- alias filter_enum_not_eq filter_not_eq
33
+ alias_method :filter_integer_not_eq, :filter_not_eq
34
+ alias_method :filter_float_not_eq, :filter_not_eq
35
+ alias_method :filter_big_decimal_not_eq, :filter_not_eq
36
+ alias_method :filter_date_not_eq, :filter_not_eq
37
+ alias_method :filter_boolean_not_eq, :filter_not_eq
38
+ alias_method :filter_uuid_not_eq, :filter_not_eq
39
+ alias_method :filter_enum_not_eq, :filter_not_eq
40
40
 
41
41
  def filter_string_eq(scope, attribute, value, is_not: false)
42
42
  column = column_for(scope, attribute)
@@ -57,54 +57,63 @@ module Graphiti
57
57
  filter_string_eql(scope, attribute, value, is_not: true)
58
58
  end
59
59
 
60
- def filter_string_prefix(scope, attribute, value, is_not: false)
61
- column = column_for(scope, attribute)
62
- map = value.map { |v| "#{v}%" }
63
- clause = column.lower.matches_any(map)
64
- is_not ? scope.where.not(clause) : scope.where(clause)
65
- end
66
-
67
- def filter_string_not_prefix(scope, attribute, value)
68
- filter_string_prefix(scope, attribute, value, is_not: true)
69
- end
70
-
71
- def filter_string_suffix(scope, attribute, value, is_not: false)
72
- column = column_for(scope, attribute)
73
- map = value.map { |v| "%#{v}" }
74
- clause = column.lower.matches_any(map)
75
- is_not ? scope.where.not(clause) : scope.where(clause)
76
- end
77
-
78
- def filter_string_not_suffix(scope, attribute, value)
79
- filter_string_suffix(scope, attribute, value, is_not: true)
80
- end
81
-
82
60
  # Arel has different match escaping behavior before rails 5.
83
61
  # Since rails 4.x does not expose methods to escape LIKE statements
84
62
  # anyway, we just don't support proper LIKE escaping in those versions.
85
- if ::ActiveRecord.version >= Gem::Version.new('5.0.0')
63
+ if ::ActiveRecord.version >= Gem::Version.new("5.0.0")
86
64
  def filter_string_match(scope, attribute, value, is_not: false)
87
- escape_char = '\\'
88
- column = column_for(scope, attribute)
89
- map = value.map do |v|
90
- v = v.downcase
91
- v = scope.sanitize_sql_like(v)
65
+ clause = sanitized_like_for(scope, attribute, value) { |v|
92
66
  "%#{v}%"
93
- end
94
- clause = column.lower.matches_any(map, escape_char, true)
67
+ }
68
+ is_not ? scope.where.not(clause) : scope.where(clause)
69
+ end
70
+
71
+ def filter_string_prefix(scope, attribute, value, is_not: false)
72
+ clause = sanitized_like_for(scope, attribute, value) { |v|
73
+ "#{v}%"
74
+ }
75
+ is_not ? scope.where.not(clause) : scope.where(clause)
76
+ end
77
+
78
+ def filter_string_suffix(scope, attribute, value, is_not: false)
79
+ clause = sanitized_like_for(scope, attribute, value) { |v|
80
+ "%#{v}"
81
+ }
95
82
  is_not ? scope.where.not(clause) : scope.where(clause)
96
83
  end
97
84
  else
98
85
  def filter_string_match(scope, attribute, value, is_not: false)
99
86
  column = column_for(scope, attribute)
100
- map = value.map do |v|
87
+ map = value.map { |v|
101
88
  "%#{v.downcase}%"
102
- end
89
+ }
90
+ clause = column.lower.matches_any(map)
91
+ is_not ? scope.where.not(clause) : scope.where(clause)
92
+ end
93
+
94
+ def filter_string_prefix(scope, attribute, value, is_not: false)
95
+ column = column_for(scope, attribute)
96
+ map = value.map { |v| "#{v}%" }
97
+ clause = column.lower.matches_any(map)
98
+ is_not ? scope.where.not(clause) : scope.where(clause)
99
+ end
100
+
101
+ def filter_string_suffix(scope, attribute, value, is_not: false)
102
+ column = column_for(scope, attribute)
103
+ map = value.map { |v| "%#{v}" }
103
104
  clause = column.lower.matches_any(map)
104
105
  is_not ? scope.where.not(clause) : scope.where(clause)
105
106
  end
106
107
  end
107
108
 
109
+ def filter_string_not_prefix(scope, attribute, value)
110
+ filter_string_prefix(scope, attribute, value, is_not: true)
111
+ end
112
+
113
+ def filter_string_not_suffix(scope, attribute, value)
114
+ filter_string_suffix(scope, attribute, value, is_not: true)
115
+ end
116
+
108
117
  def filter_string_not_match(scope, attribute, value)
109
118
  filter_string_match(scope, attribute, value, is_not: true)
110
119
  end
@@ -113,44 +122,44 @@ module Graphiti
113
122
  column = column_for(scope, attribute)
114
123
  scope.where(column.gt_any(value))
115
124
  end
116
- alias filter_integer_gt filter_gt
117
- alias filter_float_gt filter_gt
118
- alias filter_big_decimal_gt filter_gt
119
- alias filter_datetime_gt filter_gt
120
- alias filter_date_gt filter_gt
125
+ alias_method :filter_integer_gt, :filter_gt
126
+ alias_method :filter_float_gt, :filter_gt
127
+ alias_method :filter_big_decimal_gt, :filter_gt
128
+ alias_method :filter_datetime_gt, :filter_gt
129
+ alias_method :filter_date_gt, :filter_gt
121
130
 
122
131
  def filter_gte(scope, attribute, value)
123
132
  column = column_for(scope, attribute)
124
133
  scope.where(column.gteq_any(value))
125
134
  end
126
- alias filter_integer_gte filter_gte
127
- alias filter_float_gte filter_gte
128
- alias filter_big_decimal_gte filter_gte
129
- alias filter_datetime_gte filter_gte
130
- alias filter_date_gte filter_gte
135
+ alias_method :filter_integer_gte, :filter_gte
136
+ alias_method :filter_float_gte, :filter_gte
137
+ alias_method :filter_big_decimal_gte, :filter_gte
138
+ alias_method :filter_datetime_gte, :filter_gte
139
+ alias_method :filter_date_gte, :filter_gte
131
140
 
132
141
  def filter_lt(scope, attribute, value)
133
142
  column = column_for(scope, attribute)
134
143
  scope.where(column.lt_any(value))
135
144
  end
136
- alias filter_integer_lt filter_lt
137
- alias filter_float_lt filter_lt
138
- alias filter_big_decimal_lt filter_lt
139
- alias filter_datetime_lt filter_lt
140
- alias filter_date_lt filter_lt
145
+ alias_method :filter_integer_lt, :filter_lt
146
+ alias_method :filter_float_lt, :filter_lt
147
+ alias_method :filter_big_decimal_lt, :filter_lt
148
+ alias_method :filter_datetime_lt, :filter_lt
149
+ alias_method :filter_date_lt, :filter_lt
141
150
 
142
151
  def filter_lte(scope, attribute, value)
143
152
  column = column_for(scope, attribute)
144
153
  scope.where(column.lteq_any(value))
145
154
  end
146
- alias filter_integer_lte filter_lte
147
- alias filter_float_lte filter_lte
148
- alias filter_big_decimal_lte filter_lte
149
- alias filter_date_lte filter_lte
155
+ alias_method :filter_integer_lte, :filter_lte
156
+ alias_method :filter_float_lte, :filter_lte
157
+ alias_method :filter_big_decimal_lte, :filter_lte
158
+ alias_method :filter_date_lte, :filter_lte
150
159
 
151
160
  # Ensure fractional seconds don't matter
152
161
  def filter_datetime_eq(scope, attribute, value, is_not: false)
153
- ranges = value.map { |v| (v..v + 1.second - 0.00000001) }
162
+ ranges = value.map { |v| (v..v + 1.second - 0.00000001) unless v.nil? }
154
163
  clause = {attribute => ranges}
155
164
  is_not ? scope.where.not(clause) : scope.where(clause)
156
165
  end
@@ -175,14 +184,17 @@ module Graphiti
175
184
  end
176
185
 
177
186
  # (see Adapters::Abstract#paginate)
178
- def paginate(scope, current_page, per_page)
179
- scope.page(current_page).per(per_page)
187
+ def paginate(scope, current_page, per_page, offset)
188
+ scope = scope.page(current_page) if current_page
189
+ scope = scope.per(per_page) if per_page
190
+ scope = scope.padding(offset) if offset
191
+ scope
180
192
  end
181
193
 
182
194
  # (see Adapters::Abstract#count)
183
195
  def count(scope, attr)
184
196
  if attr.to_sym == :total
185
- scope.distinct.count
197
+ scope.distinct.count(:all)
186
198
  else
187
199
  scope.distinct.count(attr)
188
200
  end
@@ -231,7 +243,8 @@ module Graphiti
231
243
  children.each do |child|
232
244
  if association_type == :many_to_many &&
233
245
  [:create, :update].include?(Graphiti.context[:namespace]) &&
234
- !parent.send(association_name).exists?(child.id)
246
+ !parent.send(association_name).exists?(child.id) &&
247
+ child.errors.blank?
235
248
  parent.send(association_name) << child
236
249
  else
237
250
  target = association.instance_variable_get(:@target)
@@ -273,8 +286,8 @@ module Graphiti
273
286
 
274
287
  # (see Adapters::Abstract#update)
275
288
  def update(model_class, update_params)
276
- instance = model_class.find(update_params.delete(:id))
277
- instance.update_attributes(update_params)
289
+ instance = model_class.find(update_params.only(:id))
290
+ instance.update_attributes(update_params.except(:id))
278
291
  instance
279
292
  end
280
293
 
@@ -288,6 +301,18 @@ module Graphiti
288
301
  model_instance
289
302
  end
290
303
 
304
+ def close
305
+ ::ActiveRecord::Base.clear_active_connections!
306
+ end
307
+
308
+ def can_group?
309
+ true
310
+ end
311
+
312
+ def group(scope, attribute)
313
+ scope.group(attribute)
314
+ end
315
+
291
316
  private
292
317
 
293
318
  def column_for(scope, name)
@@ -298,6 +323,26 @@ module Graphiti
298
323
  table[name]
299
324
  end
300
325
  end
326
+
327
+ def sanitized_like_for(scope, attribute, value, &block)
328
+ escape_char = "\\"
329
+ column = column_for(scope, attribute)
330
+ map = value.map { |v|
331
+ v = v.downcase
332
+ v = Sanitizer.sanitize_like(v, escape_char)
333
+ block.call v
334
+ }
335
+
336
+ column.lower.matches_any(map, escape_char, true)
337
+ end
338
+
339
+ class Sanitizer
340
+ extend ::ActiveRecord::Sanitization::ClassMethods
341
+
342
+ def self.sanitize_like(*args)
343
+ sanitize_sql_like(*args)
344
+ end
345
+ end
301
346
  end
302
347
  end
303
348
  end
@@ -34,7 +34,7 @@ module Graphiti
34
34
 
35
35
  def build_url(scope)
36
36
  url = resource.remote_url
37
- params = scope[:params].merge(scope.except(:params))
37
+ params = scope[:params].merge(scope.except(:params, :foreign_key))
38
38
  params[:page] ||= {}
39
39
  params[:page][:size] ||= 999
40
40
  params = CGI.unescape(params.to_query)
@@ -178,7 +178,7 @@ module Graphiti
178
178
  end
179
179
 
180
180
  # (see Adapters::Abstract#paginate)
181
- def paginate(scope, current_page, per_page)
181
+ def paginate(scope, current_page, per_page, offset)
182
182
  scope
183
183
  end
184
184
 
@@ -0,0 +1,78 @@
1
+ module Graphiti
2
+ module Adapters
3
+ module Persistence
4
+ module Associations
5
+ def process_belongs_to(persistence, attributes)
6
+ parents = [].tap do |processed|
7
+ persistence.iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
8
+ id = x.dig(:attributes, :id)
9
+ x[:object] = x[:resource]
10
+ .persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
11
+ processed << x
12
+ rescue Graphiti::Errors::RecordNotFound
13
+ if Graphiti.config.raise_on_missing_sidepost
14
+ path = "relationships/#{x.dig(:meta, :jsonapi_type)}"
15
+ raise Graphiti::Errors::RecordNotFound.new(x[:sideload].name, id, path)
16
+ else
17
+ pointer = "data/relationships/#{x.dig(:meta, :jsonapi_type)}"
18
+ object = Graphiti::Errors::NullRelation.new(id.to_s, pointer)
19
+ object.errors.add(:base, :not_found, message: "could not be found")
20
+ x[:object] = object
21
+ processed << x
22
+ end
23
+ end
24
+ end
25
+
26
+ update_foreign_key_for_parents(parents, attributes)
27
+ parents
28
+ end
29
+
30
+ def process_has_many(persistence, caller_model)
31
+ [].tap do |processed|
32
+ persistence.iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
33
+ update_foreign_key(caller_model, x[:attributes], x)
34
+
35
+ x[:object] = x[:resource]
36
+ .persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model, x[:foreign_key])
37
+
38
+ processed << x
39
+ end
40
+ end
41
+ end
42
+
43
+ def update_foreign_key_for_parents(parents, attributes)
44
+ parents.each do |x|
45
+ update_foreign_key(x[:object], attributes, x)
46
+ end
47
+ end
48
+
49
+ # The child's attributes should be modified to nil-out the
50
+ # foreign_key when the parent is being destroyed or disassociated
51
+ #
52
+ # This is not the case for HABTM, whose "foreign key" is a join table
53
+ def update_foreign_key(parent_object, attrs, x)
54
+ return if x[:sideload].type == :many_to_many
55
+
56
+ if [:destroy, :disassociate].include?(x[:meta][:method])
57
+ if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many?
58
+ attrs[:"#{x[:sideload].polymorphic_as}_type"] = nil
59
+ end
60
+ attrs[x[:foreign_key]] = nil
61
+ update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
62
+ else
63
+ if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many?
64
+ attrs[:"#{x[:sideload].polymorphic_as}_type"] = parent_object.class.name
65
+ end
66
+ attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
67
+ update_foreign_type(attrs, x) if x[:is_polymorphic]
68
+ end
69
+ end
70
+
71
+ def update_foreign_type(attrs, x, null: false)
72
+ grouping_field = x[:sideload].parent.grouper.field_name
73
+ attrs[grouping_field] = null ? nil : x[:sideload].group_name
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -14,6 +14,7 @@ module Graphiti
14
14
  attr_accessor :pagination_links_on_demand
15
15
  attr_accessor :pagination_links
16
16
  attr_accessor :typecast_reads
17
+ attr_accessor :raise_on_missing_sidepost
17
18
 
18
19
  attr_reader :debug, :debug_models
19
20
 
@@ -29,6 +30,7 @@ module Graphiti
29
30
  @pagination_links_on_demand = false
30
31
  @pagination_links = false
31
32
  @typecast_reads = true
33
+ @raise_on_missing_sidepost = true
32
34
  self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
33
35
  self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)
34
36
 
@@ -43,7 +45,7 @@ module Graphiti
43
45
  end
44
46
 
45
47
  if (logger = ::Rails.logger)
46
- self.debug = logger.level.zero? && self.debug
48
+ self.debug = logger.debug? && debug
47
49
  Graphiti.logger = logger
48
50
  end
49
51
  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
 
@@ -6,32 +6,48 @@ module Graphiti
6
6
  end
7
7
 
8
8
  def links?
9
- @proxy.query.pagination_links?
9
+ @proxy.query.pagination_links? && @proxy.data.present?
10
10
  end
11
11
 
12
12
  def links
13
13
  @links ||= {}.tap do |links|
14
+ links[:self] = pagination_link(current_page)
14
15
  links[:first] = pagination_link(1)
15
16
  links[:last] = pagination_link(last_page)
16
- links[:prev] = pagination_link(current_page - 1) unless current_page == 1
17
- links[:next] = pagination_link(current_page + 1) unless current_page == last_page
18
- end.select {|k, v| !v.nil? }
17
+ links[:prev] = pagination_link(current_page - 1) if has_previous_page?
18
+ links[:next] = pagination_link(current_page + 1) if has_next_page?
19
+ end.select { |k, v| !v.nil? }
20
+ end
21
+
22
+ def has_next_page?
23
+ current_page != last_page && last_page.present?
24
+ end
25
+
26
+ def has_previous_page?
27
+ current_page != 1 ||
28
+ !!pagination_params.try(:[], :page).try(:[], :after) ||
29
+ !!pagination_params.try(:[], :page).try(:[], :offset)
19
30
  end
20
31
 
21
32
  private
22
33
 
34
+ def pagination_params
35
+ @pagination_params ||= @proxy.query.params.reject { |key, _| [:action, :controller, :format].include?(key) }
36
+ end
37
+
23
38
  def pagination_link(page)
24
39
  return nil unless @proxy.resource.endpoint
25
40
 
26
41
  uri = URI(@proxy.resource.endpoint[:url].to_s)
27
42
 
43
+ page_params = {
44
+ number: page,
45
+ size: page_size
46
+ }
47
+ page_params[:offset] = offset if offset
48
+
28
49
  # Overwrite the pagination query params with the desired page
29
- uri.query = @proxy.query.hash.merge({
30
- page: {
31
- number: page,
32
- size: page_size,
33
- },
34
- }).to_query
50
+ uri.query = pagination_params.merge(page: page_params).to_query
35
51
  uri.to_s
36
52
  end
37
53
 
@@ -41,15 +57,18 @@ module Graphiti
41
57
  elsif page_size == 0 || item_count == 0
42
58
  return nil
43
59
  end
44
- @last_page = (item_count / page_size)
45
- @last_page += 1 if item_count % page_size > 0
60
+
61
+ count = item_count
62
+ count = item_count - offset if offset
63
+ @last_page = (count / page_size)
64
+ @last_page += 1 if count % page_size > 0
46
65
  @last_page
47
66
  end
48
67
 
49
68
  def item_count
50
69
  begin
51
70
  return @item_count if @item_count
52
- @item_count = @proxy.resource.stat(:total, :count).call(@proxy.scope.unpaginated_object, :total)
71
+ @item_count = item_count_from_proxy || item_count_from_stats
53
72
  unless @item_count.is_a?(Numeric)
54
73
  raise TypeError, "#{@proxy.resource}.stat(:total, :count) returned an invalid value #{@item_count}"
55
74
  end
@@ -64,10 +83,25 @@ module Graphiti
64
83
  @item_count
65
84
  end
66
85
 
86
+ def item_count_from_proxy
87
+ @proxy.stats.dig(:total, :count)
88
+ end
89
+
90
+ def item_count_from_stats
91
+ stats = Stats::Payload.new(@proxy.resource, @proxy.query, @proxy.scope.unpaginated_object, @proxy.data)
92
+ stats.calculate_stat(:total, @proxy.resource.stat(:total, :count))
93
+ end
94
+
67
95
  def current_page
68
96
  @current_page ||= (page_param[:number] || 1).to_i
69
97
  end
70
98
 
99
+ def offset
100
+ @offset ||= if (value = page_param[:offset])
101
+ value.to_i
102
+ end
103
+ end
104
+
71
105
  def page_size
72
106
  @page_size ||= (page_param[:size] ||
73
107
  @proxy.resource.default_page_size ||
@@ -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