graphiti 1.2.16 → 1.3.9

Sign up to get free protection for your applications and to get access to all the features.
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