graphiti 1.2.31 → 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  4. data/lib/graphiti/adapters/abstract.rb +3 -2
  5. data/lib/graphiti/adapters/active_record.rb +7 -3
  6. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  7. data/lib/graphiti/adapters/null.rb +1 -1
  8. data/lib/graphiti/adapters/persistence/associations.rb +10 -2
  9. data/lib/graphiti/configuration.rb +3 -1
  10. data/lib/graphiti/delegates/pagination.rb +33 -10
  11. data/lib/graphiti/errors.rb +71 -11
  12. data/lib/graphiti/extensions/extra_attribute.rb +3 -3
  13. data/lib/graphiti/hash_renderer.rb +194 -21
  14. data/lib/graphiti/query.rb +32 -6
  15. data/lib/graphiti/renderer.rb +19 -1
  16. data/lib/graphiti/request_validators/update_validator.rb +3 -3
  17. data/lib/graphiti/request_validators/validator.rb +29 -21
  18. data/lib/graphiti/resource/configuration.rb +22 -1
  19. data/lib/graphiti/resource/dsl.rb +20 -2
  20. data/lib/graphiti/resource/interface.rb +1 -1
  21. data/lib/graphiti/resource/polymorphism.rb +6 -1
  22. data/lib/graphiti/resource/remote.rb +1 -1
  23. data/lib/graphiti/resource.rb +2 -1
  24. data/lib/graphiti/resource_proxy.rb +12 -0
  25. data/lib/graphiti/schema.rb +27 -4
  26. data/lib/graphiti/schema_diff.rb +44 -4
  27. data/lib/graphiti/scope.rb +2 -2
  28. data/lib/graphiti/scoping/filter.rb +19 -8
  29. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  30. data/lib/graphiti/scoping/paginate.rb +47 -3
  31. data/lib/graphiti/serializer.rb +41 -6
  32. data/lib/graphiti/sideload.rb +16 -1
  33. data/lib/graphiti/util/class.rb +6 -0
  34. data/lib/graphiti/util/link.rb +4 -0
  35. data/lib/graphiti/util/remote_params.rb +9 -4
  36. data/lib/graphiti/util/remote_serializer.rb +1 -0
  37. data/lib/graphiti/util/serializer_attributes.rb +37 -10
  38. data/lib/graphiti/version.rb +1 -1
  39. data/lib/graphiti.rb +2 -0
  40. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c8a84a85ead11c3f8cf69f4e1fb060ea7e9ce6f470b31181214c5f62aa6f847
4
- data.tar.gz: 52b0574208041cff9168e04e9e0946bbaa856c6dc22b1ea3f425673faffbffd5
3
+ metadata.gz: cbdb96a78aeac1f48bcea53a3b068638456e703b17fccf68d2f3813270db4bd8
4
+ data.tar.gz: c1dab2eff065e8ba70193cac81c6c5c44c57f81f9074082887ed7b52f2bcb000
5
5
  SHA512:
6
- metadata.gz: 0ea059b18be0333f2193d024758de5c4a10a0f8f4e4c71d402a1b43e4d3b2f6a3fb9f426d21facf586406178d1e92398ade8f62c3c272adfdbb057623510ee54
7
- data.tar.gz: 4fb47cd7b6a9397af3660bf1996cdc6f1a33d89d21f2bc67645189665093b0268e46776d1d5871374f7198e9549f15655e5cfc8535ce1a800b6fa4be17462f66
6
+ metadata.gz: 1a860a2d6cc8bfc494207ddd6bac1643c58ba09148fdf85b07c9198840728ace0f752fefb94bafa6599a97eb5ccfa6408544b08312aca5c6e01d77c874141821
7
+ data.tar.gz: 4a821372147d76d03a6d3e4dc5dce6ca205464d852892484567f725355459b0d5477418340ed9d53831a0062218e7b9daaf3560a0951c2decd8fd177a499954b
data/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ## Unreleased
2
2
 
3
3
  Features:
4
+ - [329](https://github.com/graphiti-api/graphiti/pull/329) Propagate `extra_fields` to related resource links.
4
5
  - [242](https://github.com/graphiti-api/graphiti/pull/242) Bump `jsonapi-renderer` to `~0.2.2` now that (https://github.com/jsonapi-rb/jsonapi-renderer/pull/36) is fixed.
5
6
  - [158](https://github.com/graphiti-api/graphiti/pull/158) Filters options `allow_nil: true`
6
7
  Option can be set at the resource level `Resource.filters_accept_nil_by_default = true`.
@@ -10,6 +11,8 @@ Features:
10
11
 
11
12
  Fixes:
12
13
  - [282] Support model names including "Resource"
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
13
16
 
14
17
  ## 1.1.0
15
18
 
@@ -12,6 +12,7 @@ module Graphiti
12
12
  def api_namespace
13
13
  @api_namespace ||= begin
14
14
  ns = graphiti_config["namespace"]
15
+ ns.delete_suffix("/")
15
16
 
16
17
  if ns.blank?
17
18
  ns = prompt \
@@ -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)
@@ -240,7 +243,8 @@ module Graphiti
240
243
  children.each do |child|
241
244
  if association_type == :many_to_many &&
242
245
  [:create, :update].include?(Graphiti.context[:namespace]) &&
243
- !parent.send(association_name).exists?(child.id)
246
+ !parent.send(association_name).exists?(child.id) &&
247
+ child.errors.blank?
244
248
  parent.send(association_name) << child
245
249
  else
246
250
  target = association.instance_variable_get(:@target)
@@ -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
 
@@ -11,8 +11,16 @@ module Graphiti
11
11
  .persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
12
12
  processed << x
13
13
  rescue Graphiti::Errors::RecordNotFound
14
- path = "relationships/#{x.dig(:meta, :jsonapi_type)}"
15
- raise Graphiti::Errors::RecordNotFound.new(x[:sideload].name, id, path)
14
+ if Graphiti.config.raise_on_missing_sidepost
15
+ path = "relationships/#{x.dig(:meta, :jsonapi_type)}"
16
+ raise Graphiti::Errors::RecordNotFound.new(x[:sideload].name, id, path)
17
+ else
18
+ pointer = "data/relationships/#{x.dig(:meta, :jsonapi_type)}"
19
+ object = Graphiti::Errors::NullRelation.new(id.to_s, pointer)
20
+ object.errors.add(:base, :not_found, message: "could not be found")
21
+ x[:object] = object
22
+ processed << x
23
+ end
16
24
  end
17
25
  end
18
26
  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? && debug
48
+ self.debug = logger.debug? && debug
47
49
  Graphiti.logger = logger
48
50
  end
49
51
  end
@@ -11,13 +11,24 @@ module Graphiti
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
17
+ links[:prev] = pagination_link(current_page - 1) if has_previous_page?
18
+ links[:next] = pagination_link(current_page + 1) if has_next_page?
18
19
  end.select { |k, v| !v.nil? }
19
20
  end
20
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
+
21
32
  private
22
33
 
23
34
  def pagination_params
@@ -29,13 +40,14 @@ module Graphiti
29
40
 
30
41
  uri = URI(@proxy.resource.endpoint[:url].to_s)
31
42
 
43
+ page_params = {
44
+ number: page,
45
+ size: page_size
46
+ }
47
+ page_params[:offset] = offset if offset
48
+
32
49
  # Overwrite the pagination query params with the desired page
33
- uri.query = pagination_params.merge({
34
- page: {
35
- number: page,
36
- size: page_size
37
- }
38
- }).to_query
50
+ uri.query = pagination_params.merge(page: page_params).to_query
39
51
  uri.to_s
40
52
  end
41
53
 
@@ -45,8 +57,11 @@ module Graphiti
45
57
  elsif page_size == 0 || item_count == 0
46
58
  return nil
47
59
  end
48
- @last_page = (item_count / page_size)
49
- @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
50
65
  @last_page
51
66
  end
52
67
 
@@ -81,6 +96,14 @@ module Graphiti
81
96
  @current_page ||= (page_param[:number] || 1).to_i
82
97
  end
83
98
 
99
+ def offset
100
+ @offset ||= begin
101
+ if (value = page_param[:offset])
102
+ value.to_i
103
+ end
104
+ end
105
+ end
106
+
84
107
  def page_size
85
108
  @page_size ||= (page_param[:size] ||
86
109
  @proxy.resource.default_page_size ||
@@ -2,6 +2,31 @@ module Graphiti
2
2
  module Errors
3
3
  class Base < StandardError; end
4
4
 
5
+ class UnreadableAttribute < Base
6
+ def initialize(resource_class, name)
7
+ @resource_class = resource_class
8
+ @name = name
9
+ end
10
+
11
+ def message
12
+ "#{@resource_class}: Requested field #{@name}, but not authorized to read it"
13
+ end
14
+ end
15
+
16
+ class NullRelation
17
+ attr_accessor :id, :errors, :pointer
18
+
19
+ def initialize(id, pointer)
20
+ @id = id
21
+ @pointer = pointer
22
+ @errors = Graphiti::Util::SimpleErrors.new(self)
23
+ end
24
+
25
+ def self.human_attribute_name(attr, options = {})
26
+ attr
27
+ end
28
+ end
29
+
5
30
  class AdapterNotImplemented < Base
6
31
  def initialize(adapter, attribute, method)
7
32
  @adapter = adapter
@@ -10,7 +35,7 @@ module Graphiti
10
35
  end
11
36
 
12
37
  def message
13
- <<-MSG
38
+ <<~MSG
14
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.
15
40
  MSG
16
41
  end
@@ -24,7 +49,7 @@ module Graphiti
24
49
  end
25
50
 
26
51
  def message
27
- <<-MSG
52
+ <<~MSG
28
53
  #{@parent_resource_class} sideload :#{@name} - #{@message}
29
54
  MSG
30
55
  end
@@ -53,7 +78,7 @@ module Graphiti
53
78
  end
54
79
 
55
80
  def message
56
- <<-MSG
81
+ <<~MSG
57
82
  #{@resource_class}: Tried to pass block to .#{@method_name}, which only accepts a method name.
58
83
  MSG
59
84
  end
@@ -65,7 +90,7 @@ module Graphiti
65
90
  end
66
91
 
67
92
  def message
68
- <<-MSG
93
+ <<~MSG
69
94
  #{@resource_class}: Tried to perform write operation. Writes are not supported for remote resources - hit the endpoint directly.
70
95
  MSG
71
96
  end
@@ -80,7 +105,7 @@ module Graphiti
80
105
  end
81
106
 
82
107
  def message
83
- <<-MSG
108
+ <<~MSG
84
109
  #{@resource.class}: Tried to filter #{@filter_name.inspect} on operator #{@operator.inspect}, but not supported! Supported operators are #{@supported}.
85
110
  MSG
86
111
  end
@@ -93,7 +118,7 @@ module Graphiti
93
118
  end
94
119
 
95
120
  def message
96
- <<-MSG
121
+ <<~MSG
97
122
  #{@sideload.parent_resource.class.name}: tried to sideload #{@sideload.name.inspect}, but more than one #{@sideload.parent_resource.model.name} was passed!
98
123
 
99
124
  This is because you marked the sideload #{@sideload.name.inspect} with single: true
@@ -114,7 +139,7 @@ module Graphiti
114
139
  end
115
140
 
116
141
  def message
117
- <<-MSG
142
+ <<~MSG
118
143
  #{@resource.class.name}: tried to sort on attribute #{@attribute.inspect}, but passed #{@direction.inspect} when only #{@allowlist.inspect} is supported.
119
144
  MSG
120
145
  end
@@ -127,7 +152,7 @@ module Graphiti
127
152
  end
128
153
 
129
154
  def message
130
- <<-MSG
155
+ <<~MSG
131
156
  #{@resource_class.name}: called .on_extra_attribute #{@name.inspect}, but extra attribute #{@name.inspect} does not exist!
132
157
  MSG
133
158
  end
@@ -148,7 +173,7 @@ module Graphiti
148
173
  else
149
174
  "value #{@value.inspect}"
150
175
  end
151
- msg = <<-MSG
176
+ msg = <<~MSG
152
177
  #{@resource.class.name}: tried to filter on #{@filter.keys[0].inspect}, but passed invalid #{value_string}.
153
178
  MSG
154
179
  msg << "\nAllowlist: #{allow.inspect}" if allow
@@ -165,7 +190,7 @@ module Graphiti
165
190
  end
166
191
 
167
192
  def message
168
- <<-MSG
193
+ <<~MSG
169
194
  #{@resource_class.name} You declared an attribute or filter of type "#{@enum_type}" without providing a list of permitted values, which is required.
170
195
 
171
196
  When declaring an attribute:
@@ -189,7 +214,7 @@ module Graphiti
189
214
  end
190
215
 
191
216
  def message
192
- <<-MSG
217
+ <<~MSG
193
218
  #{@resource_class.name}: Cannot link to sideload #{@sideload.name.inspect}!
194
219
 
195
220
  Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
@@ -708,6 +733,12 @@ module Graphiti
708
733
  end
709
734
  end
710
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
+
711
742
  class InvalidInclude < Base
712
743
  def initialize(resource, relationship)
713
744
  @resource = resource
@@ -791,5 +822,34 @@ module Graphiti
791
822
 
792
823
  class ConflictRequest < InvalidRequest
793
824
  end
825
+
826
+ class FilterGroupInvalidRequirement < Base
827
+ def initialize(resource, valid_required_values)
828
+ @resource = resource
829
+ @valid_required_values = valid_required_values
830
+ end
831
+
832
+ def message
833
+ <<-MSG.gsub(/\s+/, " ").strip
834
+ The filter group required: value on resource #{@resource.class} must be one of the following:
835
+ #{@valid_required_values.join(", ")}
836
+ MSG
837
+ end
838
+ end
839
+
840
+ class FilterGroupMissingRequiredFilters < Base
841
+ def initialize(resource, filter_names, required)
842
+ @resource = resource
843
+ @filter_names = filter_names
844
+ @required_label = required == :all ? "All" : "One"
845
+ end
846
+
847
+ def message
848
+ <<-MSG.gsub(/\s+/, " ").strip
849
+ #{@required_label} of the following filters must be provided on resource #{@resource.type}:
850
+ #{@filter_names.join(", ")}
851
+ MSG
852
+ end
853
+ end
794
854
  end
795
855
  end
@@ -46,9 +46,9 @@ module Graphiti
46
46
  next false unless instance_exec(&options[:if])
47
47
  end
48
48
 
49
- @extra_fields &&
50
- @extra_fields[@_type] &&
51
- @extra_fields[@_type].include?(name)
49
+ next false unless @extra_fields
50
+
51
+ @extra_fields[@_type]&.include?(name) || @extra_fields[@resource&.type]&.include?(name)
52
52
  }
53
53
 
54
54
  attribute name, if: allow_field, &blk
@@ -1,55 +1,228 @@
1
1
  module Graphiti
2
2
  module SerializableHash
3
- def to_hash(fields: nil, include: {})
3
+ def to_hash(fields: nil, include: {}, name_chain: [], graphql: false)
4
4
  {}.tap do |hash|
5
- fields_list = fields[jsonapi_type] if fields
5
+ if fields
6
+ fields_list = nil
7
+
8
+ # Dot syntax wins over jsonapi type
9
+ if name_chain.length > 0
10
+ fields_list = fields[name_chain.join(".").to_sym]
11
+ end
12
+
13
+ if fields_list.nil?
14
+ fields_list = fields[jsonapi_type]
15
+ end
16
+ end
17
+
18
+ # polymorphic resources - merge the PARENT type
19
+ if polymorphic_subclass?
20
+ if fields[@resource.type]
21
+ fields_list ||= []
22
+ fields_list |= fields[@resource.type]
23
+ end
24
+
25
+ if fields[jsonapi_type]
26
+ fields_list ||= []
27
+ fields_list |= fields[jsonapi_type]
28
+ end
29
+ end
30
+
6
31
  attrs = requested_attributes(fields_list).each_with_object({}) { |(k, v), h|
7
- h[k] = instance_eval(&v)
32
+ name = graphql ? k.to_s.camelize(:lower).to_sym : k
33
+ h[name] = instance_eval(&v)
34
+ }
35
+
36
+ # The main logic here is just !!include[k]
37
+ # But we also have special on__<type>--<name> includes
38
+ # Where we only include when matching the polymorphic type
39
+ rels = @_relationships.select { |k, v|
40
+ if include[k]
41
+ true
42
+ else
43
+ included = false
44
+ include.keys.each do |key|
45
+ split = key.to_s.split(/^on__/)
46
+ if split.length > 1
47
+ requested_type, key = split[1].split("--")
48
+ if requested_type.to_sym == jsonapi_type
49
+ included = k == key.to_sym
50
+ break
51
+ end
52
+ end
53
+ end
54
+ included
55
+ end
8
56
  }
9
- rels = @_relationships.select { |k, v| !!include[k] }
57
+
10
58
  rels.each_with_object({}) do |(k, v), h|
59
+ nested_include = include[k]
60
+
61
+ # This logic only fires if it's a special on__<type>--<name> include
62
+ unless include.has_key?(k)
63
+ include.keys.each do |include_key|
64
+ if k == include_key.to_s.split("--")[1].to_sym
65
+ nested_include = include[include_key]
66
+ break
67
+ end
68
+ end
69
+ end
70
+
11
71
  serializers = v.send(:resources)
12
- attrs[k] = if serializers.is_a?(Array)
13
- serializers.map do |rr| # use private method to avoid array casting
14
- rr.to_hash(fields: fields, include: include[k])
72
+ name = graphql ? k.to_s.camelize(:lower) : k
73
+ name_chain = name_chain.dup
74
+ name_chain << k unless name_chain.last == k
75
+
76
+ unless remote_resource? && serializers.nil?
77
+ payload = if serializers.is_a?(Array)
78
+ data = serializers.map { |rr|
79
+ rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
80
+ }
81
+ graphql ? {nodes: data} : data
82
+ elsif serializers.nil?
83
+ if @resource.class.respond_to?(:sideload)
84
+ if @resource.class.sideload(k).type.to_s.include?("_many")
85
+ graphql ? {nodes: []} : []
86
+ end
87
+ end
88
+ else
89
+ serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
15
90
  end
16
- elsif serializers.nil?
17
- nil
18
- else
19
- serializers.to_hash(fields: fields, include: include[k])
91
+
92
+ attrs[name.to_sym] = payload
93
+ end
94
+ end
95
+
96
+ if !graphql || (fields_list || []).include?(:id)
97
+ hash[:id] = jsonapi_id
98
+ end
99
+
100
+ if (fields_list || []).include?(:_type)
101
+ hash[:_type] = jsonapi_type.to_s
102
+ end
103
+
104
+ if (fields_list || []).include?(:_cursor)
105
+ hash[:_cursor] = cursor
106
+ end
107
+
108
+ if (fields_list || []).include?(:__typename)
109
+ resource_class = @resource.class
110
+ if polymorphic_subclass?
111
+ resource_class = @resource.class.resource_for_type(jsonapi_type)
20
112
  end
113
+ hash[:__typename] = ::Graphiti::Util::Class
114
+ .graphql_type_name(resource_class.name)
21
115
  end
22
116
 
23
- hash[:id] = jsonapi_id
24
117
  hash.merge!(attrs) if attrs.any?
25
118
  end
26
119
  end
120
+
121
+ def polymorphic_subclass?
122
+ !remote_resource? &&
123
+ @resource.polymorphic? &&
124
+ @resource.type != jsonapi_type
125
+ end
126
+
127
+ # See hack in util/remote_serializer.rb
128
+ def remote_resource?
129
+ @resource == 1
130
+ end
27
131
  end
28
132
 
29
133
  class HashRenderer
30
- def initialize(resource)
134
+ def initialize(resource, graphql: false)
31
135
  @resource = resource
136
+ @graphql = graphql
32
137
  end
33
138
 
34
139
  def render(options)
35
140
  serializers = options[:data]
36
141
  opts = options.slice(:fields, :include)
37
- to_hash(serializers, opts).tap do |hash|
38
- hash.merge!(options.slice(:meta)) unless options[:meta].empty?
142
+ opts[:graphql] = @graphql
143
+ top_level_key = get_top_level_key(@resource, serializers.is_a?(Array))
144
+
145
+ hash = {top_level_key => {}}
146
+ nodes = get_nodes(serializers, opts)
147
+ add_nodes(hash, top_level_key, options, nodes, @graphql)
148
+ add_stats(hash, top_level_key, options, @graphql)
149
+ if @graphql
150
+ add_page_info(hash, serializers, top_level_key, options)
39
151
  end
152
+
153
+ hash
40
154
  end
41
155
 
42
156
  private
43
157
 
44
- def to_hash(serializers, opts)
45
- {}.tap do |hash|
46
- hash[:data] = if serializers.is_a?(Array)
47
- serializers.map do |s|
48
- s.to_hash(**opts)
158
+ def get_top_level_key(resource, is_many)
159
+ key = :data
160
+
161
+ if @graphql
162
+ key = @resource.graphql_entrypoint
163
+ key = key.to_s.singularize.to_sym unless is_many
164
+ end
165
+
166
+ key
167
+ end
168
+
169
+ def get_nodes(serializers, opts)
170
+ if serializers.is_a?(Array)
171
+ serializers.each_with_index.map do |s, index|
172
+ s.to_hash(**opts)
173
+ end
174
+ else
175
+ serializers.to_hash(**opts)
176
+ end
177
+ end
178
+
179
+ def add_nodes(hash, top_level_key, opts, nodes, graphql)
180
+ payload = nodes
181
+ if graphql && nodes.is_a?(Array)
182
+ payload = {nodes: nodes}
183
+ end
184
+
185
+ # Don't render nodes if we only requested stats
186
+ unless graphql && opts[:fields].values == [[:stats]]
187
+ hash[top_level_key] = payload
188
+ end
189
+ end
190
+
191
+ def add_stats(hash, top_level_key, options, graphql)
192
+ if options[:meta] && !options[:meta].empty?
193
+ if @graphql
194
+ if (stats = options[:meta][:stats])
195
+ hash[top_level_key][:stats] = stats
49
196
  end
50
197
  else
51
- serializers.to_hash(**opts)
198
+ hash.merge!(options.slice(:meta))
199
+ end
200
+ end
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)
52
223
  end
224
+
225
+ hash[top_level_key][:pageInfo] = info
53
226
  end
54
227
  end
55
228
  end