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
@@ -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
@@ -96,7 +98,18 @@ module Graphiti
96
98
  sl_resource = resource_for_sideload(sideload)
97
99
  query_parents = parents + [self]
98
100
  sub_hash = sub_hash[:include] if sub_hash.key?(:include)
99
- hash[key] = Query.new(sl_resource, @params, key, sub_hash, query_parents, :all)
101
+
102
+ # NB: To handle on__<type>--<name>
103
+ # A) relationship_name == :positions
104
+ # B) key == on__employees.positions
105
+ # This way A) ensures sideloads are resolved
106
+ # And B) ensures nested filters, sorts etc still work
107
+ relationship_name = sideload ? sideload.name : key
108
+ hash[relationship_name] = Query.new sl_resource,
109
+ @params,
110
+ key,
111
+ sub_hash,
112
+ query_parents, :all
100
113
  else
101
114
  handle_missing_sideload(key)
102
115
  end
@@ -178,12 +191,13 @@ module Graphiti
178
191
  (@params[:page] || {}).each_pair do |name, value|
179
192
  if legacy_nested?(name)
180
193
  value.each_pair do |k, v|
181
- hash[k.to_sym] = v.to_i
194
+ hash[k.to_sym] = cast_page_param(k.to_sym, v)
182
195
  end
183
196
  elsif nested?(name)
184
- hash[name.to_s.split(".").last.to_sym] = value
185
- elsif top_level? && [:number, :size].include?(name.to_sym)
186
- 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)
187
201
  end
188
202
  end
189
203
  end
@@ -227,6 +241,18 @@ module Graphiti
227
241
 
228
242
  private
229
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
+
230
256
  # Try to find on this resource
231
257
  # If not there, follow the legacy logic of scalling all other
232
258
  # resource names/types
@@ -17,8 +17,20 @@ module Graphiti
17
17
  render(self.class.jsonapi_renderer).to_json
18
18
  end
19
19
 
20
+ def as_graphql
21
+ render(self.class.graphql_renderer(@proxy))
22
+ end
23
+
24
+ def to_graphql
25
+ as_graphql.to_json
26
+ end
27
+
20
28
  def to_json
21
- render(self.class.hash_renderer(@proxy)).to_json
29
+ as_json.to_json
30
+ end
31
+
32
+ def as_json
33
+ render(self.class.hash_renderer(@proxy))
22
34
  end
23
35
 
24
36
  def to_xml
@@ -35,6 +47,11 @@ module Graphiti
35
47
  JSONAPI::Serializable::Renderer.new(implementation)
36
48
  end
37
49
 
50
+ def self.graphql_renderer(proxy)
51
+ implementation = Graphiti::HashRenderer.new(proxy.resource, graphql: true)
52
+ JSONAPI::Serializable::Renderer.new(implementation)
53
+ end
54
+
38
55
  private
39
56
 
40
57
  def render(renderer)
@@ -49,6 +66,7 @@ module Graphiti
49
66
  options[:meta] ||= proxy.meta
50
67
  options[:meta][:stats] = proxy.stats unless proxy.stats.empty?
51
68
  options[:meta][:debug] = Debugger.to_a if debug_json?
69
+ options[:proxy] = proxy
52
70
 
53
71
  renderer.render(records, options)
54
72
  end
@@ -26,17 +26,17 @@ module Graphiti
26
26
  [:data, :type],
27
27
  [:data, :id]
28
28
  ].each do |required_attr|
29
- attribute_mismatch(required_attr) unless @raw_params.dig(*required_attr)
29
+ attribute_mismatch(required_attr) unless @params.dig(*required_attr)
30
30
  end
31
31
  errors.blank?
32
32
  end
33
33
 
34
34
  def payload_matches_endpoint?
35
- unless @raw_params.dig(:data, :id) == @raw_params.dig(:filter, :id)
35
+ unless @params.dig(:data, :id) == @params.dig(:filter, :id)
36
36
  attribute_mismatch([:data, :id])
37
37
  end
38
38
 
39
- meta_type = @raw_params.dig(:data, :type)
39
+ meta_type = @params.dig(:data, :type)
40
40
 
41
41
  # NOTE: calling #to_s and comparing 2 strings is slower than
42
42
  # calling #to_sym and comparing 2 symbols. But pre ruby-2.2
@@ -5,21 +5,31 @@ module Graphiti
5
5
 
6
6
  def initialize(root_resource, raw_params, action)
7
7
  @root_resource = root_resource
8
- @raw_params = raw_params
8
+ @params = normalized_params(raw_params)
9
9
  @errors = Graphiti::Util::SimpleErrors.new(raw_params)
10
10
  @action = action
11
11
  end
12
12
 
13
13
  def validate
14
+ # Right now, all requests - even reads - go through the validator
15
+ # In the future these should have their own validation logic, but
16
+ # for now we can just bypass
17
+ return true unless @params.has_key?(:data)
18
+
14
19
  resource = @root_resource
15
- if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
16
- if @root_resource.type != meta_type && @root_resource.polymorphic?
17
- resource = @root_resource.class.resource_for_type(meta_type).new
20
+
21
+ if @params[:data].has_key?(:type)
22
+ if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
23
+ if @root_resource.type != meta_type && @root_resource.polymorphic?
24
+ resource = @root_resource.class.resource_for_type(meta_type).new
25
+ end
18
26
  end
19
- end
20
27
 
21
- typecast_attributes(resource, deserialized_payload.attributes, deserialized_payload.meta[:payload_path])
22
- process_relationships(resource, deserialized_payload.relationships, deserialized_payload.meta[:payload_path])
28
+ typecast_attributes(resource, deserialized_payload.attributes, @action, deserialized_payload.meta[:payload_path])
29
+ process_relationships(resource, deserialized_payload.relationships, deserialized_payload.meta[:payload_path])
30
+ else
31
+ errors.add(:"data.type", :missing)
32
+ end
23
33
 
24
34
  errors.blank?
25
35
  end
@@ -33,14 +43,7 @@ module Graphiti
33
43
  end
34
44
 
35
45
  def deserialized_payload
36
- @deserialized_payload ||= begin
37
- payload = normalized_params
38
- if payload[:data] && payload[:data][:type]
39
- Graphiti::Deserializer.new(payload)
40
- else
41
- Graphiti::Deserializer.new({})
42
- end
43
- end
46
+ @deserialized_payload ||= Graphiti::Deserializer.new(@params)
44
47
  end
45
48
 
46
49
  private
@@ -62,15 +65,20 @@ module Graphiti
62
65
  next
63
66
  end
64
67
 
65
- typecast_attributes(x[:resource], x[:attributes], x[:meta][:payload_path])
66
- process_relationships(x[:resource], x[:relationships], x[:meta][:payload_path])
68
+ resource = x[:resource]
69
+ attributes = x[:attributes]
70
+ relationships = x[:relationships]
71
+ payload_path = x[:meta][:payload_path]
72
+ action = x[:meta][:method]
73
+ typecast_attributes(resource, attributes, action, payload_path)
74
+ process_relationships(resource, relationships, payload_path)
67
75
  end
68
76
  end
69
77
 
70
- def typecast_attributes(resource, attributes, payload_path)
78
+ def typecast_attributes(resource, attributes, action, payload_path)
71
79
  attributes.each_pair do |key, value|
72
80
  # Only validate id if create action, otherwise it's only used for lookup
73
- next if @action != :create &&
81
+ next if action != :create &&
74
82
  key == :id &&
75
83
  resource.class.config[:attributes][:id][:writable] == false
76
84
 
@@ -86,8 +94,8 @@ module Graphiti
86
94
  end
87
95
  end
88
96
 
89
- def normalized_params
90
- normalized = @raw_params
97
+ def normalized_params(raw_params)
98
+ normalized = raw_params
91
99
  if normalized.respond_to?(:to_unsafe_h)
92
100
  normalized = normalized.to_unsafe_h.deep_symbolize_keys
93
101
  end
@@ -28,6 +28,14 @@ module Graphiti
28
28
  end
29
29
  end
30
30
 
31
+ def graphql_entrypoint=(val)
32
+ if val
33
+ super(val.to_s.camelize(:lower).to_sym)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
31
39
  # The .stat call stores a proc based on adapter
32
40
  # So if we assign a new adapter, reconfigure
33
41
  def adapter=(val)
@@ -83,7 +91,9 @@ module Graphiti
83
91
  :relationships_readable_by_default,
84
92
  :relationships_writable_by_default,
85
93
  :filters_accept_nil_by_default,
86
- :filters_deny_empty_by_default
94
+ :filters_deny_empty_by_default,
95
+ :graphql_entrypoint,
96
+ :cursor_paginatable
87
97
 
88
98
  class << self
89
99
  prepend Overrides
@@ -97,6 +107,7 @@ module Graphiti
97
107
  # re-assigning causes a new Class.new
98
108
  klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
99
109
  klass.type ||= klass.infer_type
110
+ klass.graphql_entrypoint = klass.type.to_s.pluralize.to_sym
100
111
  default(klass, :attributes_readable_by_default, true)
101
112
  default(klass, :attributes_writable_by_default, true)
102
113
  default(klass, :attributes_sortable_by_default, true)
@@ -144,6 +155,7 @@ module Graphiti
144
155
  if (@abstract_class = val)
145
156
  self.serializer = nil
146
157
  self.type = nil
158
+ self.graphql_entrypoint = nil
147
159
  end
148
160
  end
149
161
 
@@ -188,6 +200,7 @@ module Graphiti
188
200
  @config ||=
189
201
  {
190
202
  filters: {},
203
+ grouped_filters: {},
191
204
  default_filters: {},
192
205
  stats: {},
193
206
  sort_all: nil,
@@ -224,6 +237,10 @@ module Graphiti
224
237
  config[:filters]
225
238
  end
226
239
 
240
+ def grouped_filters
241
+ config[:grouped_filters]
242
+ end
243
+
227
244
  def sorts
228
245
  config[:sorts]
229
246
  end
@@ -262,6 +279,10 @@ module Graphiti
262
279
  self.class.filters
263
280
  end
264
281
 
282
+ def grouped_filters
283
+ self.class.grouped_filters
284
+ end
285
+
265
286
  def sort_all
266
287
  self.class.sort_all
267
288
  end
@@ -9,7 +9,11 @@ module Graphiti
9
9
  opts = args.extract_options!
10
10
  type_override = args[0]
11
11
 
12
- if (att = get_attr(name, :filterable, raise_error: :only_unsupported))
12
+ if (att = (attributes[name] || extra_attributes[name]))
13
+ # We're opting in to filtering, so force this
14
+ # UNLESS the filter is guarded at the attribute level
15
+ att[:filterable] = true if att[:filterable] == false
16
+
13
17
  aliases = [name, opts[:aliases]].flatten.compact
14
18
  operators = FilterOperators.build(self, att[:type], opts, &blk)
15
19
 
@@ -23,6 +27,8 @@ module Graphiti
23
27
  end
24
28
 
25
29
  required = att[:filterable] == :required || !!opts[:required]
30
+ schema = !!opts[:via_attribute_dsl] ? att[:schema] : opts[:schema] != false
31
+
26
32
  config[:filters][name.to_sym] = {
27
33
  aliases: aliases,
28
34
  name: name.to_sym,
@@ -32,6 +38,7 @@ module Graphiti
32
38
  single: !!opts[:single],
33
39
  dependencies: opts[:dependent],
34
40
  required: required,
41
+ schema: schema,
35
42
  operators: operators.to_hash,
36
43
  allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default),
37
44
  deny_empty: opts.fetch(:deny_empty, filters_deny_empty_by_default)
@@ -44,6 +51,17 @@ module Graphiti
44
51
  end
45
52
  end
46
53
 
54
+ def filter_group(filter_names, *args)
55
+ opts = args.extract_options!
56
+
57
+ Scoping::FilterGroupValidator.raise_unless_filter_group_requirement_valid!(self, opts[:required])
58
+
59
+ config[:grouped_filters] = {
60
+ names: filter_names,
61
+ required: opts[:required]
62
+ }
63
+ end
64
+
47
65
  def sort_all(&blk)
48
66
  if block_given?
49
67
  config[:_sort_all] = blk
@@ -119,7 +137,7 @@ module Graphiti
119
137
  options[:sortable] ? sort(name) : config[:sorts].delete(name)
120
138
 
121
139
  if options[:filterable]
122
- filter(name, allow: options[:allow])
140
+ filter(name, allow: options[:allow], via_attribute_dsl: true)
123
141
  else
124
142
  config[:filters].delete(name)
125
143
  end
@@ -46,7 +46,7 @@ module Graphiti
46
46
  private
47
47
 
48
48
  def validate!(params)
49
- return unless validate_endpoints?
49
+ return if Graphiti.context[:graphql] || !validate_endpoints?
50
50
 
51
51
  if context&.respond_to?(:request)
52
52
  path = context.request.env["PATH_INFO"]
@@ -42,9 +42,14 @@ module Graphiti
42
42
  end
43
43
 
44
44
  def sideload(name)
45
- sl = super
45
+ if (split_on = name.to_s.split(/^on__/)).length > 1
46
+ on_type, name = split_on[1].split("--").map(&:to_sym)
47
+ end
48
+
49
+ sl = super(name)
46
50
  if !polymorphic_child? && sl.nil?
47
51
  children.each do |c|
52
+ next if on_type && c.type != on_type
48
53
  break if (sl = c.sideloads[name])
49
54
  end
50
55
  end
@@ -31,7 +31,7 @@ module Graphiti
31
31
  end
32
32
 
33
33
  def before_resolve(scope, query)
34
- scope[:params] = Util::RemoteParams.generate(self, query)
34
+ scope[:params] = Util::RemoteParams.generate(self, query, scope[:foreign_key])
35
35
  scope
36
36
  end
37
37
 
@@ -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
 
@@ -47,10 +47,22 @@ module Graphiti
47
47
  Renderer.new(self, options).to_json
48
48
  end
49
49
 
50
+ def as_json(options = {})
51
+ Renderer.new(self, options).as_json
52
+ end
53
+
50
54
  def to_xml(options = {})
51
55
  Renderer.new(self, options).to_xml
52
56
  end
53
57
 
58
+ def to_graphql(options = {})
59
+ Renderer.new(self, options).to_graphql
60
+ end
61
+
62
+ def as_graphql(options = {})
63
+ Renderer.new(self, options).as_graphql
64
+ end
65
+
54
66
  def data
55
67
  @data ||= begin
56
68
  records = @scope.resolve
@@ -7,6 +7,7 @@ module Graphiti
7
7
  ::Rails.application.eager_load! if defined?(::Rails)
8
8
  resources ||= Graphiti.resources.reject(&:abstract_class?)
9
9
  resources.reject! { |r| r.name.nil? }
10
+
10
11
  new(resources).generate
11
12
  end
12
13
 
@@ -25,7 +26,7 @@ module Graphiti
25
26
 
26
27
  def initialize(resources)
27
28
  @resources = resources.sort_by(&:name)
28
- @remote_resources = resources.select(&:remote?)
29
+ @remote_resources = @resources.select(&:remote?)
29
30
  @local_resources = @resources - @remote_resources
30
31
  end
31
32
 
@@ -89,14 +90,20 @@ module Graphiti
89
90
  config = {
90
91
  name: r.name,
91
92
  type: r.type.to_s,
93
+ graphql_entrypoint: r.graphql_entrypoint.to_s,
92
94
  description: r.description,
93
95
  attributes: attributes(r),
94
96
  extra_attributes: extra_attributes(r),
95
97
  sorts: sorts(r),
96
98
  filters: filters(r),
97
- relationships: relationships(r)
99
+ relationships: relationships(r),
100
+ stats: stats(r)
98
101
  }
99
102
 
103
+ if r.grouped_filters.any?
104
+ config[:filter_group] = r.grouped_filters
105
+ end
106
+
100
107
  if r.default_sort
101
108
  default_sort = r.default_sort.map { |s|
102
109
  {s.keys.first.to_s => s.values.first.to_s}
@@ -108,7 +115,7 @@ module Graphiti
108
115
  config[:default_page_size] = r.default_page_size
109
116
  end
110
117
 
111
- if r.polymorphic?
118
+ if r.polymorphic? && !r.polymorphic_child?
112
119
  config[:polymorphic] = true
113
120
  config[:children] = r.children.map(&:name)
114
121
  end
@@ -163,6 +170,14 @@ module Graphiti
163
170
  end
164
171
  end
165
172
 
173
+ def stats(resource)
174
+ {}.tap do |stats|
175
+ resource.stats.each_pair do |name, config|
176
+ stats[name] = config.calculations.keys
177
+ end
178
+ end
179
+ end
180
+
166
181
  def sorts(resource)
167
182
  {}.tap do |s|
168
183
  resource.sorts.each_pair do |name, sort|
@@ -182,7 +197,7 @@ module Graphiti
182
197
  def filters(resource)
183
198
  {}.tap do |f|
184
199
  resource.filters.each_pair do |name, filter|
185
- next unless resource.attributes[name][:schema]
200
+ next unless resource.filters[name][:schema]
186
201
 
187
202
  config = {
188
203
  type: filter[:type].to_s,
@@ -202,11 +217,18 @@ module Graphiti
202
217
  config[:guard] = true
203
218
  end
204
219
  end
220
+ if filter[:required] # one-off filter, not attribute
221
+ config[:required] = true
222
+ end
205
223
  f[name] = config
206
224
  end
207
225
  end
208
226
  end
209
227
 
228
+ def filter_group(resource)
229
+ resource.config[:grouped_filters]
230
+ end
231
+
210
232
  def relationships(resource)
211
233
  {}.tap do |r|
212
234
  resource.sideloads.each_pair do |name, config|
@@ -214,6 +236,7 @@ module Graphiti
214
236
  if config.type == :polymorphic_belongs_to
215
237
  schema[:resources] = config.children.values
216
238
  .map(&:resource).map(&:class).map(&:name)
239
+ schema[:parent_resource] = config.parent_resource.class.name
217
240
  else
218
241
  schema[:resource] = config.resource.class.name
219
242
  end
@@ -1,8 +1,8 @@
1
1
  module Graphiti
2
2
  class SchemaDiff
3
3
  def initialize(old, new)
4
- @old = old.deep_symbolize_keys
5
- @new = new.deep_symbolize_keys
4
+ @old = JSON.parse(old.to_json).deep_symbolize_keys
5
+ @new = JSON.parse(new.to_json).deep_symbolize_keys
6
6
  @errors = []
7
7
  end
8
8
 
@@ -30,6 +30,8 @@ module Graphiti
30
30
  compare_extra_attributes(r, new_resource)
31
31
  compare_sorts(r, new_resource)
32
32
  compare_filters(r, new_resource)
33
+ compare_filter_group(r, new_resource)
34
+ compare_stats(r, new_resource)
33
35
  compare_relationships(r, new_resource)
34
36
  end
35
37
  end
@@ -133,12 +135,12 @@ module Graphiti
133
135
  end
134
136
 
135
137
  if new_sort[:only] && !old_sort[:only]
136
- @errors << "#{old_resource[:name]}: sort #{name.inspect} now limited to only #{new_sort[:only].inspect}."
138
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} now limited to only #{new_sort[:only].to_sym.inspect}."
137
139
  end
138
140
 
139
141
  if new_sort[:only] && old_sort[:only]
140
142
  if new_sort[:only] != old_sort[:only]
141
- @errors << "#{old_resource[:name]}: sort #{name.inspect} was limited to only #{old_sort[:only].inspect}, now limited to only #{new_sort[:only].inspect}."
143
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} was limited to only #{old_sort[:only].to_sym.inspect}, now limited to only #{new_sort[:only].to_sym.inspect}."
142
144
  end
143
145
  end
144
146
  end
@@ -204,6 +206,44 @@ module Graphiti
204
206
  end
205
207
  end
206
208
 
209
+ def compare_filter_group(old_resource, new_resource)
210
+ if new_resource[:filter_group]
211
+ if old_resource[:filter_group]
212
+ new_names = new_resource[:filter_group][:names]
213
+ old_names = old_resource[:filter_group][:names]
214
+ diff = new_names - old_names
215
+ if !diff.empty? && new_resource[:filter_group][:required] == "all"
216
+ @errors << "#{old_resource[:name]}: all required filter group #{old_names.map(&:to_sym).inspect} added #{"member".pluralize(diff.length)} #{diff.map(&:to_sym).inspect}."
217
+ end
218
+
219
+ old_required = old_resource[:filter_group][:required]
220
+ new_required = new_resource[:filter_group][:required]
221
+ if old_required == "any" && new_required == "all"
222
+ @errors << "#{old_resource[:name]}: filter group #{old_names.map(&:to_sym).inspect} moved from required: :any to required: :all"
223
+ end
224
+ else
225
+ @errors << "#{old_resource[:name]}: filter group #{new_resource[:filter_group][:names].map(&:to_sym).inspect} was added."
226
+ end
227
+ end
228
+ end
229
+
230
+ def compare_stats(old_resource, new_resource)
231
+ return unless old_resource.key?(:stats)
232
+
233
+ old_resource[:stats].each_pair do |name, old_calculations|
234
+ new_calculations = new_resource[:stats][name]
235
+ if new_calculations
236
+ old_calculations.each do |calc|
237
+ unless new_calculations.include?(calc)
238
+ @errors << "#{old_resource[:name]}: calculation #{calc.to_sym.inspect} was removed from stat #{name.inspect}."
239
+ end
240
+ end
241
+ else
242
+ @errors << "#{old_resource[:name]}: stat #{name.inspect} was removed."
243
+ end
244
+ end
245
+ end
246
+
207
247
  def compare_endpoints
208
248
  @old[:endpoints].each_pair do |path, old_endpoint|
209
249
  unless (new_endpoint = @new[:endpoints][path])
@@ -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
 
@@ -3,6 +3,13 @@ module Graphiti
3
3
  include Scoping::Filterable
4
4
 
5
5
  def apply
6
+ unless @opts[:bypass_required_filters]
7
+ Graphiti::Scoping::FilterGroupValidator.new(
8
+ resource,
9
+ query_hash
10
+ ).raise_unless_filter_group_requirements_met!
11
+ end
12
+
6
13
  if missing_required_filters.any? && !@opts[:bypass_required_filters]
7
14
  raise Errors::RequiredFilter.new(resource, missing_required_filters)
8
15
  end
@@ -164,14 +171,18 @@ module Graphiti
164
171
  type = Graphiti::Types[filter[:type]]
165
172
  array_or_string = [:string, :array].include?(type[:canonical_name])
166
173
  if (arr = value.scan(/\[.*?\]/)).present? && array_or_string
167
- value = arr.map { |json|
168
- begin
169
- JSON.parse(json)
170
- rescue
171
- raise Errors::InvalidJSONArray.new(resource, value)
172
- end
173
- }
174
- 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
175
186
  else
176
187
  value = parse_string_arrays(value, !!filter[:single])
177
188
  end