graphiti 1.2.31 → 1.3.4

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 (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