graphiti 1.2.41 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71f32a89c9442ab11825e9fef78897d0b3a6b3b91e4cca1c714b074c6cb009b6
4
- data.tar.gz: faeaaab30063200334a6e8f9d0203a4b7274d5dba2918a7ae8143e4e234cf58c
3
+ metadata.gz: c694cce2123c7ba03ae53b53b61b9aaa5c353681e28a93722bc73569845ea2e2
4
+ data.tar.gz: 2305bdf6566d1bcba126dbe893dd64f6f011367151e0a283185fba62781d0d02
5
5
  SHA512:
6
- metadata.gz: c5b9868ae015f0e881940bada465fc62bf859b61921a4e2c78c7dad8f86be8ef8cb7c6826d02b5e94ad2c3d36a4c9b73c7d0b5c5a1c31cf231e8becd62da202f
7
- data.tar.gz: 9f027306154a0fc833459182c14386424fcedbf196f53784c203ad0f42fb76190b890c7f7782e092d64da0cc05751a70de6eaa085543ab73dc70d55fea363c4a
6
+ metadata.gz: 3d9d20b0b87720079461b0ce245970da308362616cb4c21c4232fb85c585a62694f3bdb874db3cf8d80e94e02ff3078c119e0a0a46e95dbdb94e439dbf5a1af8
7
+ data.tar.gz: 9cf83060d95a7789351f3f0979ccf46012238975248159d8a5fccd0e457b512cfc127b3a4295c46f668b80717dfec2152fea025d00a82a1ffcdd42edde9e5c7f
@@ -244,14 +244,15 @@ module Graphiti
244
244
  # @param scope The scope object we are chaining
245
245
  # @param [Integer] current_page The current page number
246
246
  # @param [Integer] per_page The number of results per page
247
+ # @param [Integer] offset The offset to start from
247
248
  # @return the scope
248
249
  #
249
250
  # @example ActiveRecord default
250
251
  # # via kaminari gem
251
- # def paginate(scope, current_page, per_page)
252
+ # def paginate(scope, current_page, per_page, offset)
252
253
  # scope.page(current_page).per(per_page)
253
254
  # end
254
- def paginate(scope, current_page, per_page)
255
+ def paginate(scope, current_page, per_page, offset)
255
256
  raise "you must override #paginate in an adapter subclass"
256
257
  end
257
258
 
@@ -184,8 +184,11 @@ module Graphiti
184
184
  end
185
185
 
186
186
  # (see Adapters::Abstract#paginate)
187
- def paginate(scope, current_page, per_page)
188
- 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
189
192
  end
190
193
 
191
194
  # (see Adapters::Abstract#count)
@@ -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
 
@@ -14,11 +14,19 @@ module Graphiti
14
14
  links[:self] = pagination_link(current_page)
15
15
  links[:first] = pagination_link(1)
16
16
  links[:last] = pagination_link(last_page)
17
- links[:prev] = pagination_link(current_page - 1) unless current_page == 1
18
- links[:next] = pagination_link(current_page + 1) unless current_page == last_page
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
19
  end.select { |k, v| !v.nil? }
20
20
  end
21
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
+ end
29
+
22
30
  private
23
31
 
24
32
  def pagination_params
@@ -30,13 +38,14 @@ module Graphiti
30
38
 
31
39
  uri = URI(@proxy.resource.endpoint[:url].to_s)
32
40
 
41
+ page_params = {
42
+ number: page,
43
+ size: page_size
44
+ }
45
+ page_params[:offset] = offset if offset
46
+
33
47
  # Overwrite the pagination query params with the desired page
34
- uri.query = pagination_params.merge({
35
- page: {
36
- number: page,
37
- size: page_size
38
- }
39
- }).to_query
48
+ uri.query = pagination_params.merge(page: page_params).to_query
40
49
  uri.to_s
41
50
  end
42
51
 
@@ -46,8 +55,11 @@ module Graphiti
46
55
  elsif page_size == 0 || item_count == 0
47
56
  return nil
48
57
  end
49
- @last_page = (item_count / page_size)
50
- @last_page += 1 if item_count % page_size > 0
58
+
59
+ count = item_count
60
+ count = item_count - offset if offset
61
+ @last_page = (count / page_size)
62
+ @last_page += 1 if count % page_size > 0
51
63
  @last_page
52
64
  end
53
65
 
@@ -82,6 +94,14 @@ module Graphiti
82
94
  @current_page ||= (page_param[:number] || 1).to_i
83
95
  end
84
96
 
97
+ def offset
98
+ @offset ||= begin
99
+ if (value = page_param[:offset])
100
+ value.to_i
101
+ end
102
+ end
103
+ end
104
+
85
105
  def page_size
86
106
  @page_size ||= (page_param[:size] ||
87
107
  @proxy.resource.default_page_size ||
@@ -733,6 +733,12 @@ module Graphiti
733
733
  end
734
734
  end
735
735
 
736
+ class UnsupportedBeforeCursor < Base
737
+ def message
738
+ "Passing in page[before] and page[number] is not supported. Please create an issue if you need it!"
739
+ end
740
+ end
741
+
736
742
  class InvalidInclude < Base
737
743
  def initialize(resource, relationship)
738
744
  @resource = resource
@@ -101,6 +101,10 @@ module Graphiti
101
101
  hash[:_type] = jsonapi_type.to_s
102
102
  end
103
103
 
104
+ if (fields_list || []).include?(:_cursor)
105
+ hash[:_cursor] = cursor
106
+ end
107
+
104
108
  if (fields_list || []).include?(:__typename)
105
109
  resource_class = @resource.class
106
110
  if polymorphic_subclass?
@@ -142,6 +146,10 @@ module Graphiti
142
146
  nodes = get_nodes(serializers, opts)
143
147
  add_nodes(hash, top_level_key, options, nodes, @graphql)
144
148
  add_stats(hash, top_level_key, options, @graphql)
149
+ if @graphql
150
+ add_page_info(hash, serializers, top_level_key, options)
151
+ end
152
+
145
153
  hash
146
154
  end
147
155
 
@@ -160,7 +168,7 @@ module Graphiti
160
168
 
161
169
  def get_nodes(serializers, opts)
162
170
  if serializers.is_a?(Array)
163
- serializers.map do |s|
171
+ serializers.each_with_index.map do |s, index|
164
172
  s.to_hash(**opts)
165
173
  end
166
174
  else
@@ -191,5 +199,31 @@ module Graphiti
191
199
  end
192
200
  end
193
201
  end
202
+
203
+ # NB - this is only for top-level right now
204
+ # The casing here is GQL-specific, we can update later if needed.
205
+ def add_page_info(hash, serializers, top_level_key, options)
206
+ if (fields = options[:fields].try(:[], :page_info))
207
+ info = {}
208
+
209
+ if fields.include?(:has_next_page)
210
+ info[:hasNextPage] = options[:proxy].pagination.has_next_page?
211
+ end
212
+
213
+ if fields.include?(:has_previous_page)
214
+ info[:hasPreviousPage] = options[:proxy].pagination.has_previous_page?
215
+ end
216
+
217
+ if fields.include?(:start_cursor)
218
+ info[:startCursor] = serializers.first.cursor
219
+ end
220
+
221
+ if fields.include?(:end_cursor)
222
+ info[:endCursor] = serializers.last.cursor
223
+ end
224
+
225
+ hash[top_level_key][:pageInfo] = info
226
+ end
227
+ end
194
228
  end
195
229
  end
@@ -32,7 +32,9 @@ module Graphiti
32
32
  end
33
33
 
34
34
  def pagination_links?
35
- if Graphiti.config.pagination_links_on_demand
35
+ if action == :find
36
+ false
37
+ elsif Graphiti.config.pagination_links_on_demand
36
38
  [true, "true"].include?(@params[:pagination_links])
37
39
  else
38
40
  Graphiti.config.pagination_links
@@ -189,12 +191,13 @@ module Graphiti
189
191
  (@params[:page] || {}).each_pair do |name, value|
190
192
  if legacy_nested?(name)
191
193
  value.each_pair do |k, v|
192
- hash[k.to_sym] = v.to_i
194
+ hash[k.to_sym] = cast_page_param(k.to_sym, v)
193
195
  end
194
196
  elsif nested?(name)
195
- hash[name.to_s.split(".").last.to_sym] = value
196
- elsif top_level? && [:number, :size].include?(name.to_sym)
197
- hash[name.to_sym] = value.to_i
197
+ param_name = name.to_s.split(".").last.to_sym
198
+ hash[param_name] = cast_page_param(param_name, value)
199
+ elsif top_level? && Scoping::Paginate::PARAMS.include?(name.to_sym)
200
+ hash[name.to_sym] = cast_page_param(name.to_sym, value)
198
201
  end
199
202
  end
200
203
  end
@@ -238,6 +241,18 @@ module Graphiti
238
241
 
239
242
  private
240
243
 
244
+ def cast_page_param(name, value)
245
+ if [:before, :after].include?(name)
246
+ decode_cursor(value)
247
+ else
248
+ value.to_i
249
+ end
250
+ end
251
+
252
+ def decode_cursor(cursor)
253
+ JSON.parse(Base64.decode64(cursor)).symbolize_keys
254
+ end
255
+
241
256
  # Try to find on this resource
242
257
  # If not there, follow the legacy logic of scalling all other
243
258
  # resource names/types
@@ -66,6 +66,7 @@ module Graphiti
66
66
  options[:meta] ||= proxy.meta
67
67
  options[:meta][:stats] = proxy.stats unless proxy.stats.empty?
68
68
  options[:meta][:debug] = Debugger.to_a if debug_json?
69
+ options[:proxy] = proxy
69
70
 
70
71
  renderer.render(records, options)
71
72
  end
@@ -28,11 +28,12 @@ module Graphiti
28
28
  serializer
29
29
  end
30
30
 
31
- def decorate_record(record)
31
+ def decorate_record(record, index = nil)
32
32
  unless record.instance_variable_get(:@__graphiti_serializer)
33
33
  serializer = serializer_for(record)
34
34
  record.instance_variable_set(:@__graphiti_serializer, serializer)
35
35
  record.instance_variable_set(:@__graphiti_resource, self)
36
+ record.instance_variable_set(:@__graphiti_index, index) if index
36
37
  end
37
38
  end
38
39
 
@@ -92,7 +92,8 @@ module Graphiti
92
92
  :relationships_writable_by_default,
93
93
  :filters_accept_nil_by_default,
94
94
  :filters_deny_empty_by_default,
95
- :graphql_entrypoint
95
+ :graphql_entrypoint,
96
+ :cursor_paginatable
96
97
 
97
98
  class << self
98
99
  prepend Overrides
@@ -85,8 +85,8 @@ module Graphiti
85
85
  # Used to ensure the resource's serializer is used
86
86
  # Not one derived through the usual jsonapi-rb logic
87
87
  def assign_serializer(records)
88
- records.each do |r|
89
- @resource.decorate_record(r)
88
+ records.each_with_index do |r, index|
89
+ @resource.decorate_record(r, index)
90
90
  end
91
91
  end
92
92
 
@@ -171,14 +171,18 @@ module Graphiti
171
171
  type = Graphiti::Types[filter[:type]]
172
172
  array_or_string = [:string, :array].include?(type[:canonical_name])
173
173
  if (arr = value.scan(/\[.*?\]/)).present? && array_or_string
174
- value = arr.map { |json|
175
- begin
176
- JSON.parse(json)
177
- rescue
178
- raise Errors::InvalidJSONArray.new(resource, value)
179
- end
180
- }
181
- value = value[0] if value.length == 1
174
+ begin
175
+ value = arr.map { |json|
176
+ begin
177
+ JSON.parse(json)
178
+ rescue
179
+ raise Errors::InvalidJSONArray.new(resource, value)
180
+ end
181
+ }
182
+ value = value[0] if value.length == 1
183
+ rescue Errors::InvalidJSONArray => e
184
+ raise(e) if type[:canonical_name] == :array
185
+ end
182
186
  else
183
187
  value = parse_string_arrays(value, !!filter[:single])
184
188
  end
@@ -1,6 +1,7 @@
1
1
  module Graphiti
2
2
  class Scoping::Paginate < Scoping::Base
3
3
  DEFAULT_PAGE_SIZE = 20
4
+ PARAMS = [:number, :size, :offset, :before, :after]
4
5
 
5
6
  def apply
6
7
  if size > resource.max_page_size
@@ -33,24 +34,67 @@ module Graphiti
33
34
 
34
35
  # Apply default pagination proc via the Resource adapter
35
36
  def apply_standard_scope
36
- resource.adapter.paginate(@scope, number, size)
37
+ meth = resource.adapter.method(:paginate)
38
+
39
+ if meth.arity == 4 # backwards-compat
40
+ resource.adapter.paginate(@scope, number, size, offset)
41
+ else
42
+ resource.adapter.paginate(@scope, number, size)
43
+ end
37
44
  end
38
45
 
39
46
  # Apply the custom pagination proc
40
47
  def apply_custom_scope
41
- resource.instance_exec(@scope, number, size, resource.context, &custom_scope)
48
+ resource.instance_exec \
49
+ @scope,
50
+ number,
51
+ size,
52
+ resource.context,
53
+ offset,
54
+ &custom_scope
42
55
  end
43
56
 
44
57
  private
45
58
 
46
59
  def requested?
47
- ![page_param[:size], page_param[:number]].all?(&:nil?)
60
+ !PARAMS.map { |p| page_param[p] }.all?(&:nil?)
48
61
  end
49
62
 
50
63
  def page_param
51
64
  @page_param ||= (query_hash[:page] || {})
52
65
  end
53
66
 
67
+ def offset
68
+ offset = nil
69
+
70
+ if (value = page_param[:offset])
71
+ offset = value.to_i
72
+ end
73
+
74
+ if before_cursor&.key?(:offset)
75
+ if page_param.key?(:number)
76
+ raise Errors::UnsupportedBeforeCursor
77
+ end
78
+
79
+ offset = before_cursor[:offset] - (size * number) - 1
80
+ offset = 0 if offset.negative?
81
+ end
82
+
83
+ if after_cursor&.key?(:offset)
84
+ offset = after_cursor[:offset]
85
+ end
86
+
87
+ offset
88
+ end
89
+
90
+ def after_cursor
91
+ page_param[:after]
92
+ end
93
+
94
+ def before_cursor
95
+ page_param[:before]
96
+ end
97
+
54
98
  def number
55
99
  (page_param[:number] || 1).to_i
56
100
  end
@@ -45,6 +45,23 @@ module Graphiti
45
45
  end
46
46
  end
47
47
 
48
+ def cursor
49
+ starting_offset = 0
50
+ page_param = @proxy.query.pagination
51
+ if (page_number = page_param[:number])
52
+ page_size = page_param[:size] || @resource.default_page_size
53
+ starting_offset = (page_number - 1) * page_size
54
+ end
55
+
56
+ if (cursor = page_param[:after])
57
+ starting_offset = cursor[:offset]
58
+ end
59
+
60
+ current_offset = @object.instance_variable_get(:@__graphiti_index)
61
+ offset = starting_offset + current_offset + 1 # (+ 1 b/c o-base index)
62
+ Base64.encode64({offset: offset}.to_json).chomp
63
+ end
64
+
48
65
  def as_jsonapi(kwargs = {})
49
66
  super(**kwargs).tap do |hash|
50
67
  strip_relationships!(hash) if strip_relationships?
@@ -28,6 +28,12 @@ module Graphiti
28
28
 
29
29
  existing = @serializer.send(applied_method)
30
30
  @serializer.send(:"#{applied_method}=", [@name] | existing)
31
+
32
+ @serializer.meta do
33
+ if !!@resource.try(:cursor_paginatable?) && !Graphiti.context[:graphql]
34
+ {cursor: cursor}
35
+ end
36
+ end
31
37
  end
32
38
 
33
39
  private
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.2.41"
2
+ VERSION = "1.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.41
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-23 00:00:00.000000000 Z
11
+ date: 2021-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable
@@ -361,7 +361,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
361
361
  - !ruby/object:Gem::Version
362
362
  version: '0'
363
363
  requirements: []
364
- rubygems_version: 3.0.6
364
+ rubygems_version: 3.1.4
365
365
  signing_key:
366
366
  specification_version: 4
367
367
  summary: Easily build jsonapi.org-compatible APIs