graphiti 1.2.42 → 1.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a282326a13780606dc7c66a393aecb4815e8230d166c21e522e4f64566fb7abe
4
- data.tar.gz: efc784934b7091bba5839fe20c5052a5ee00198bc9a008f1330ab373ff26e965
3
+ metadata.gz: 6a4d4d39d07565c8576f7294c334b391b69a6c4c20710b7901b189eab967b60b
4
+ data.tar.gz: 2f63bf884fb57ff25e748f656651c61b9c93e8175719f5e0757a49f5fbd55752
5
5
  SHA512:
6
- metadata.gz: 396020335f1edb7b9a733cceaebecd3213e065567d83ca9c1380198967622a64927e3e9564715f4d3d76619a631005c87535fce7d094600a1d9d1b5cfd0cf42f
7
- data.tar.gz: 64058843e61f8b50f5dfa44ed9835f73012ef6bb5c2c29b2c668d20b213571b59503c790c5ba62044f2300b8f0c3f49fd7159618e0ddc9280b12aee5236bf113
6
+ metadata.gz: 87becc989b7082cbbf6464396f68249effddf376a2a1380cea9e1575e30213d15b4b370e9bea33a0ad42ddbd9fcdae038a1032940a4d0a66ac4783450c43c865
7
+ data.tar.gz: ed7c25d26c72f1abbe709ca49959ce9f6e6277b190cabcb8028faf65b736da5a94955818a5e0a2f28e572fee4d8ac5b6df17c19f2694d482b1cff7d6f035fa7c
data/CHANGELOG.md CHANGED
@@ -12,6 +12,7 @@ Features:
12
12
  Fixes:
13
13
  - [282] Support model names including "Resource"
14
14
  - [313](https://github.com/graphiti-api/graphiti/pull/313) Sort remote resources in schema generation
15
+ - [374](https://github.com/graphiti-api/graphiti/pull/374) Trim leading spaces from error messages
15
16
 
16
17
  ## 1.1.0
17
18
 
@@ -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,21 @@ 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
+ !!pagination_params.try(:[], :page).try(:[], :after) ||
29
+ !!pagination_params.try(:[], :page).try(:[], :offset)
30
+ end
31
+
22
32
  private
23
33
 
24
34
  def pagination_params
@@ -30,13 +40,14 @@ module Graphiti
30
40
 
31
41
  uri = URI(@proxy.resource.endpoint[:url].to_s)
32
42
 
43
+ page_params = {
44
+ number: page,
45
+ size: page_size
46
+ }
47
+ page_params[:offset] = offset if offset
48
+
33
49
  # 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
50
+ uri.query = pagination_params.merge(page: page_params).to_query
40
51
  uri.to_s
41
52
  end
42
53
 
@@ -46,8 +57,11 @@ module Graphiti
46
57
  elsif page_size == 0 || item_count == 0
47
58
  return nil
48
59
  end
49
- @last_page = (item_count / page_size)
50
- @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
51
65
  @last_page
52
66
  end
53
67
 
@@ -82,6 +96,14 @@ module Graphiti
82
96
  @current_page ||= (page_param[:number] || 1).to_i
83
97
  end
84
98
 
99
+ def offset
100
+ @offset ||= begin
101
+ if (value = page_param[:offset])
102
+ value.to_i
103
+ end
104
+ end
105
+ end
106
+
85
107
  def page_size
86
108
  @page_size ||= (page_param[:size] ||
87
109
  @proxy.resource.default_page_size ||
@@ -35,7 +35,7 @@ module Graphiti
35
35
  end
36
36
 
37
37
  def message
38
- <<-MSG
38
+ <<~MSG
39
39
  The adapter #{@adapter.class} does not implement method '#{@method}', which was requested for attribute '#{@attribute}'. Add this method to your adapter to support this filter operator.
40
40
  MSG
41
41
  end
@@ -49,7 +49,7 @@ module Graphiti
49
49
  end
50
50
 
51
51
  def message
52
- <<-MSG
52
+ <<~MSG
53
53
  #{@parent_resource_class} sideload :#{@name} - #{@message}
54
54
  MSG
55
55
  end
@@ -78,7 +78,7 @@ module Graphiti
78
78
  end
79
79
 
80
80
  def message
81
- <<-MSG
81
+ <<~MSG
82
82
  #{@resource_class}: Tried to pass block to .#{@method_name}, which only accepts a method name.
83
83
  MSG
84
84
  end
@@ -90,7 +90,7 @@ module Graphiti
90
90
  end
91
91
 
92
92
  def message
93
- <<-MSG
93
+ <<~MSG
94
94
  #{@resource_class}: Tried to perform write operation. Writes are not supported for remote resources - hit the endpoint directly.
95
95
  MSG
96
96
  end
@@ -105,7 +105,7 @@ module Graphiti
105
105
  end
106
106
 
107
107
  def message
108
- <<-MSG
108
+ <<~MSG
109
109
  #{@resource.class}: Tried to filter #{@filter_name.inspect} on operator #{@operator.inspect}, but not supported! Supported operators are #{@supported}.
110
110
  MSG
111
111
  end
@@ -118,7 +118,7 @@ module Graphiti
118
118
  end
119
119
 
120
120
  def message
121
- <<-MSG
121
+ <<~MSG
122
122
  #{@sideload.parent_resource.class.name}: tried to sideload #{@sideload.name.inspect}, but more than one #{@sideload.parent_resource.model.name} was passed!
123
123
 
124
124
  This is because you marked the sideload #{@sideload.name.inspect} with single: true
@@ -139,7 +139,7 @@ module Graphiti
139
139
  end
140
140
 
141
141
  def message
142
- <<-MSG
142
+ <<~MSG
143
143
  #{@resource.class.name}: tried to sort on attribute #{@attribute.inspect}, but passed #{@direction.inspect} when only #{@allowlist.inspect} is supported.
144
144
  MSG
145
145
  end
@@ -152,7 +152,7 @@ module Graphiti
152
152
  end
153
153
 
154
154
  def message
155
- <<-MSG
155
+ <<~MSG
156
156
  #{@resource_class.name}: called .on_extra_attribute #{@name.inspect}, but extra attribute #{@name.inspect} does not exist!
157
157
  MSG
158
158
  end
@@ -173,7 +173,7 @@ module Graphiti
173
173
  else
174
174
  "value #{@value.inspect}"
175
175
  end
176
- msg = <<-MSG
176
+ msg = <<~MSG
177
177
  #{@resource.class.name}: tried to filter on #{@filter.keys[0].inspect}, but passed invalid #{value_string}.
178
178
  MSG
179
179
  msg << "\nAllowlist: #{allow.inspect}" if allow
@@ -190,7 +190,7 @@ module Graphiti
190
190
  end
191
191
 
192
192
  def message
193
- <<-MSG
193
+ <<~MSG
194
194
  #{@resource_class.name} You declared an attribute or filter of type "#{@enum_type}" without providing a list of permitted values, which is required.
195
195
 
196
196
  When declaring an attribute:
@@ -214,7 +214,7 @@ module Graphiti
214
214
  end
215
215
 
216
216
  def message
217
- <<-MSG
217
+ <<~MSG
218
218
  #{@resource_class.name}: Cannot link to sideload #{@sideload.name.inspect}!
219
219
 
220
220
  Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
@@ -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.try(:cursor)
219
+ end
220
+
221
+ if fields.include?(:end_cursor)
222
+ info[:endCursor] = serializers.last.try(:cursor)
223
+ end
224
+
225
+ hash[top_level_key][:pageInfo] = info
226
+ end
227
+ end
194
228
  end
195
229
  end
@@ -32,10 +32,10 @@ module Graphiti
32
32
  end
33
33
 
34
34
  def pagination_links?
35
- if Graphiti.config.pagination_links_on_demand
36
- [true, "true"].include?(@params[:pagination_links])
37
- elsif action == :find
35
+ if action == :find
38
36
  false
37
+ elsif Graphiti.config.pagination_links_on_demand
38
+ [true, "true"].include?(@params[:pagination_links])
39
39
  else
40
40
  Graphiti.config.pagination_links
41
41
  end
@@ -191,12 +191,13 @@ module Graphiti
191
191
  (@params[:page] || {}).each_pair do |name, value|
192
192
  if legacy_nested?(name)
193
193
  value.each_pair do |k, v|
194
- hash[k.to_sym] = v.to_i
194
+ hash[k.to_sym] = cast_page_param(k.to_sym, v)
195
195
  end
196
196
  elsif nested?(name)
197
- hash[name.to_s.split(".").last.to_sym] = value
198
- elsif top_level? && [:number, :size].include?(name.to_sym)
199
- 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)
200
201
  end
201
202
  end
202
203
  end
@@ -240,6 +241,18 @@ module Graphiti
240
241
 
241
242
  private
242
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
+
243
256
  # Try to find on this resource
244
257
  # If not there, follow the legacy logic of scalling all other
245
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.42"
2
+ VERSION = "1.3.1"
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.42
4
+ version: 1.3.1
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-05-09 00:00:00.000000000 Z
11
+ date: 2021-08-17 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.3
364
+ rubygems_version: 3.2.22
365
365
  signing_key:
366
366
  specification_version: 4
367
367
  summary: Easily build jsonapi.org-compatible APIs