graphiti 1.2.16 → 1.3.9

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.standard.yml +4 -4
  4. data/Appraisals +23 -17
  5. data/CHANGELOG.md +7 -1
  6. data/Guardfile +5 -5
  7. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  8. data/deprecated_generators/graphiti/resource_generator.rb +1 -1
  9. data/gemfiles/{rails_5.gemfile → rails_5_2.gemfile} +2 -2
  10. data/gemfiles/{rails_5_graphiti_rails.gemfile → rails_5_2_graphiti_rails.gemfile} +3 -4
  11. data/gemfiles/rails_6.gemfile +1 -1
  12. data/gemfiles/rails_6_graphiti_rails.gemfile +2 -3
  13. data/gemfiles/{rails_4.gemfile → rails_7.gemfile} +2 -2
  14. data/gemfiles/rails_7_graphiti_rails.gemfile +19 -0
  15. data/graphiti.gemspec +16 -16
  16. data/lib/graphiti/adapters/abstract.rb +20 -5
  17. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +1 -1
  18. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +1 -1
  19. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +1 -1
  20. data/lib/graphiti/adapters/active_record/{inferrence.rb → inference.rb} +2 -2
  21. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +19 -0
  22. data/lib/graphiti/adapters/active_record.rb +119 -74
  23. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  24. data/lib/graphiti/adapters/null.rb +1 -1
  25. data/lib/graphiti/adapters/persistence/associations.rb +78 -0
  26. data/lib/graphiti/configuration.rb +3 -1
  27. data/lib/graphiti/debugger.rb +12 -8
  28. data/lib/graphiti/delegates/pagination.rb +47 -13
  29. data/lib/graphiti/deserializer.rb +3 -3
  30. data/lib/graphiti/errors.rb +109 -15
  31. data/lib/graphiti/extensions/extra_attribute.rb +4 -4
  32. data/lib/graphiti/extensions/temp_id.rb +1 -1
  33. data/lib/graphiti/filter_operators.rb +0 -1
  34. data/lib/graphiti/hash_renderer.rb +198 -21
  35. data/lib/graphiti/query.rb +105 -73
  36. data/lib/graphiti/railtie.rb +5 -5
  37. data/lib/graphiti/renderer.rb +19 -1
  38. data/lib/graphiti/request_validator.rb +10 -10
  39. data/lib/graphiti/request_validators/update_validator.rb +4 -5
  40. data/lib/graphiti/request_validators/validator.rb +38 -24
  41. data/lib/graphiti/resource/configuration.rb +35 -7
  42. data/lib/graphiti/resource/dsl.rb +34 -8
  43. data/lib/graphiti/resource/interface.rb +13 -3
  44. data/lib/graphiti/resource/links.rb +3 -3
  45. data/lib/graphiti/resource/persistence.rb +2 -1
  46. data/lib/graphiti/resource/polymorphism.rb +8 -2
  47. data/lib/graphiti/resource/remote.rb +2 -2
  48. data/lib/graphiti/resource/sideloading.rb +4 -4
  49. data/lib/graphiti/resource.rb +12 -1
  50. data/lib/graphiti/resource_proxy.rb +23 -3
  51. data/lib/graphiti/runner.rb +5 -5
  52. data/lib/graphiti/schema.rb +36 -11
  53. data/lib/graphiti/schema_diff.rb +44 -4
  54. data/lib/graphiti/scope.rb +8 -10
  55. data/lib/graphiti/scoping/base.rb +3 -3
  56. data/lib/graphiti/scoping/filter.rb +36 -15
  57. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  58. data/lib/graphiti/scoping/paginate.rb +47 -3
  59. data/lib/graphiti/scoping/sort.rb +5 -7
  60. data/lib/graphiti/serializer.rb +49 -7
  61. data/lib/graphiti/sideload/belongs_to.rb +1 -1
  62. data/lib/graphiti/sideload/has_many.rb +19 -1
  63. data/lib/graphiti/sideload/many_to_many.rb +11 -4
  64. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -4
  65. data/lib/graphiti/sideload.rb +47 -23
  66. data/lib/graphiti/stats/dsl.rb +0 -1
  67. data/lib/graphiti/stats/payload.rb +12 -9
  68. data/lib/graphiti/types.rb +15 -15
  69. data/lib/graphiti/util/attribute_check.rb +1 -1
  70. data/lib/graphiti/util/class.rb +6 -0
  71. data/lib/graphiti/util/link.rb +10 -2
  72. data/lib/graphiti/util/persistence.rb +21 -78
  73. data/lib/graphiti/util/relationship_payload.rb +4 -4
  74. data/lib/graphiti/util/remote_params.rb +9 -4
  75. data/lib/graphiti/util/remote_serializer.rb +1 -0
  76. data/lib/graphiti/util/serializer_attributes.rb +41 -11
  77. data/lib/graphiti/util/simple_errors.rb +4 -4
  78. data/lib/graphiti/util/transaction_hooks_recorder.rb +1 -1
  79. data/lib/graphiti/version.rb +1 -1
  80. data/lib/graphiti.rb +6 -3
  81. metadata +46 -37
  82. data/.travis.yml +0 -59
@@ -1,8 +1,8 @@
1
1
  module Graphiti
2
2
  class Query
3
- attr_reader :resource, :association_name, :params
3
+ attr_reader :resource, :association_name, :params, :action
4
4
 
5
- def initialize(resource, params, association_name = nil, nested_include = nil, parents = [])
5
+ def initialize(resource, params, association_name = nil, nested_include = nil, parents = [], action = nil)
6
6
  @resource = resource
7
7
  @association_name = association_name
8
8
  @params = params
@@ -11,6 +11,7 @@ module Graphiti
11
11
  @params = @params.deep_symbolize_keys
12
12
  @include_param = nested_include || @params[:include]
13
13
  @parents = parents
14
+ @action = parse_action(action)
14
15
  end
15
16
 
16
17
  def association?
@@ -31,7 +32,9 @@ module Graphiti
31
32
  end
32
33
 
33
34
  def pagination_links?
34
- if Graphiti.config.pagination_links_on_demand
35
+ if action == :find
36
+ false
37
+ elsif Graphiti.config.pagination_links_on_demand
35
38
  [true, "true"].include?(@params[:pagination_links])
36
39
  else
37
40
  Graphiti.config.pagination_links
@@ -44,16 +47,19 @@ module Graphiti
44
47
 
45
48
  def hash
46
49
  @hash ||= {}.tap do |h|
47
- h[:filter] = filters unless filters.empty?
48
- h[:sort] = sorts unless sorts.empty?
49
- h[:page] = pagination unless pagination.empty?
50
- unless association?
51
- h[:fields] = fields unless fields.empty?
52
- h[:extra_fields] = extra_fields unless extra_fields.empty?
50
+ h[:filter] = filters
51
+ h[:sort] = sorts
52
+ h[:page] = pagination
53
+ if association?
54
+ resource_type = @resource.class.type
55
+ h[:extra_fields] = {resource_type => extra_fields[resource_type]} if extra_fields.key?(resource_type)
56
+ else
57
+ h[:fields] = fields
58
+ h[:extra_fields] = extra_fields
53
59
  end
54
- h[:stats] = stats unless stats.empty?
55
- h[:include] = sideload_hash unless sideload_hash.empty?
56
- end
60
+ h[:stats] = stats
61
+ h[:include] = sideload_hash
62
+ end.reject { |_, value| value.empty? }
57
63
  end
58
64
 
59
65
  def zero_results?
@@ -63,11 +69,9 @@ module Graphiti
63
69
  end
64
70
 
65
71
  def sideload_hash
66
- @sideload_hash = begin
67
- {}.tap do |hash|
68
- sideloads.each_pair do |key, value|
69
- hash[key] = sideloads[key].hash
70
- end
72
+ @sideload_hash = {}.tap do |hash|
73
+ sideloads.each_pair do |key, value|
74
+ hash[key] = sideloads[key].hash
71
75
  end
72
76
  end
73
77
  end
@@ -83,19 +87,28 @@ module Graphiti
83
87
  end
84
88
 
85
89
  def sideloads
86
- @sideloads ||= begin
87
- {}.tap do |hash|
88
- include_hash.each_pair do |key, sub_hash|
89
- sideload = @resource.class.sideload(key)
90
-
91
- if sideload || @resource.remote?
92
- sl_resource = resource_for_sideload(sideload)
93
- query_parents = parents + [self]
94
- sub_hash = sub_hash[:include] if sub_hash.key?(:include)
95
- hash[key] = Query.new(sl_resource, @params, key, sub_hash, query_parents)
96
- else
97
- handle_missing_sideload(key)
98
- end
90
+ @sideloads ||= {}.tap do |hash|
91
+ include_hash.each_pair do |key, sub_hash|
92
+ sideload = @resource.class.sideload(key)
93
+
94
+ if sideload || @resource.remote?
95
+ sl_resource = resource_for_sideload(sideload)
96
+ query_parents = parents + [self]
97
+ sub_hash = sub_hash[:include] if sub_hash.key?(:include)
98
+
99
+ # NB: To handle on__<type>--<name>
100
+ # A) relationship_name == :positions
101
+ # B) key == on__employees.positions
102
+ # This way A) ensures sideloads are resolved
103
+ # And B) ensures nested filters, sorts etc still work
104
+ relationship_name = sideload ? sideload.name : key
105
+ hash[relationship_name] = Query.new sl_resource,
106
+ @params,
107
+ key,
108
+ sub_hash,
109
+ query_parents, :all
110
+ else
111
+ handle_missing_sideload(key)
99
112
  end
100
113
  end
101
114
  end
@@ -120,27 +133,25 @@ module Graphiti
120
133
  end
121
134
 
122
135
  def filters
123
- @filters ||= begin
124
- {}.tap do |hash|
125
- (@params[:filter] || {}).each_pair do |name, value|
126
- name = name.to_sym
127
-
128
- if legacy_nested?(name)
129
- value.keys.each do |key|
130
- filter_name = key.to_sym
131
- filter_value = value[key]
132
-
133
- if @resource.get_attr!(filter_name, :filterable, request: true)
134
- hash[filter_name] = filter_value
135
- end
136
+ @filters ||= {}.tap do |hash|
137
+ (@params[:filter] || {}).each_pair do |name, value|
138
+ name = name.to_sym
139
+
140
+ if legacy_nested?(name)
141
+ value.keys.each do |key|
142
+ filter_name = key.to_sym
143
+ filter_value = value[key]
144
+
145
+ if @resource.get_attr!(filter_name, :filterable, request: true)
146
+ hash[filter_name] = filter_value
136
147
  end
137
- elsif nested?(name)
138
- name = name.to_s.split(".").last.to_sym
139
- validate!(name, :filterable)
140
- hash[name] = value
141
- elsif top_level? && validate!(name, :filterable)
142
- hash[name] = value
143
148
  end
149
+ elsif nested?(name)
150
+ name = name.to_s.split(".").last.to_sym
151
+ validate!(name, :filterable)
152
+ hash[name] = value
153
+ elsif top_level? && validate!(name, :filterable)
154
+ hash[name] = value
144
155
  end
145
156
  end
146
157
  end
@@ -169,18 +180,17 @@ module Graphiti
169
180
  end
170
181
 
171
182
  def pagination
172
- @pagination ||= begin
173
- {}.tap do |hash|
174
- (@params[:page] || {}).each_pair do |name, value|
175
- if legacy_nested?(name)
176
- value.each_pair do |k, v|
177
- hash[k.to_sym] = v.to_i
178
- end
179
- elsif nested?(name)
180
- hash[name.to_s.split(".").last.to_sym] = value
181
- elsif top_level? && [:number, :size].include?(name.to_sym)
182
- hash[name.to_sym] = value.to_i
183
+ @pagination ||= {}.tap do |hash|
184
+ (@params[:page] || {}).each_pair do |name, value|
185
+ if legacy_nested?(name)
186
+ value.each_pair do |k, v|
187
+ hash[k.to_sym] = cast_page_param(k.to_sym, v)
183
188
  end
189
+ elsif nested?(name)
190
+ param_name = name.to_s.split(".").last.to_sym
191
+ hash[param_name] = cast_page_param(param_name, value)
192
+ elsif top_level? && Scoping::Paginate::PARAMS.include?(name.to_sym)
193
+ hash[name.to_sym] = cast_page_param(name.to_sym, value)
184
194
  end
185
195
  end
186
196
  end
@@ -203,15 +213,13 @@ module Graphiti
203
213
  end
204
214
 
205
215
  def stats
206
- @stats ||= begin
207
- {}.tap do |hash|
208
- (@params[:stats] || {}).each_pair do |k, v|
209
- if legacy_nested?(k)
210
- raise NotImplementedError.new("Association statistics are not currently supported")
211
- elsif top_level?
212
- v = v.split(",") if v.is_a?(String)
213
- hash[k.to_sym] = Array(v).flatten.map(&:to_sym)
214
- end
216
+ @stats ||= {}.tap do |hash|
217
+ (@params[:stats] || {}).each_pair do |k, v|
218
+ if legacy_nested?(k)
219
+ raise NotImplementedError.new("Association statistics are not currently supported")
220
+ elsif top_level?
221
+ v = v.split(",") if v.is_a?(String)
222
+ hash[k.to_sym] = Array(v).flatten.map(&:to_sym)
215
223
  end
216
224
  end
217
225
  end
@@ -223,6 +231,18 @@ module Graphiti
223
231
 
224
232
  private
225
233
 
234
+ def cast_page_param(name, value)
235
+ if [:before, :after].include?(name)
236
+ decode_cursor(value)
237
+ else
238
+ value.to_i
239
+ end
240
+ end
241
+
242
+ def decode_cursor(cursor)
243
+ JSON.parse(Base64.decode64(cursor)).symbolize_keys
244
+ end
245
+
226
246
  # Try to find on this resource
227
247
  # If not there, follow the legacy logic of scalling all other
228
248
  # resource names/types
@@ -232,7 +252,7 @@ module Graphiti
232
252
  return true if @resource.remote?
233
253
 
234
254
  if (att = @resource.get_attr(name, flag, request: true))
235
- return att
255
+ att
236
256
  else
237
257
  not_associated_name = !@resource.class.association_names.include?(name)
238
258
  not_associated_type = !@resource.class.association_types.include?(name)
@@ -262,8 +282,8 @@ module Graphiti
262
282
  def parse_fieldset(fieldset)
263
283
  {}.tap do |hash|
264
284
  fieldset.each_pair do |type, fields|
265
- type = type.to_sym
266
- fields = fields.split(",") unless fields.is_a?(Array)
285
+ type = type.to_sym
286
+ fields = fields.split(",") unless fields.is_a?(Array)
267
287
  hash[type] = fields.map(&:to_sym)
268
288
  end
269
289
  end
@@ -282,7 +302,7 @@ module Graphiti
282
302
 
283
303
  def sort_hash(attr)
284
304
  value = attr[0] == "-" ? :desc : :asc
285
- key = attr.sub("-", "").to_sym
305
+ key = attr.sub("-", "").to_sym
286
306
 
287
307
  {key => value}
288
308
  end
@@ -312,5 +332,17 @@ module Graphiti
312
332
  end
313
333
  end
314
334
  end
335
+
336
+ def parse_action(action)
337
+ action ||= @params.fetch(:action, Graphiti.context[:namespace]).try(:to_sym)
338
+ case action
339
+ when :index
340
+ :all
341
+ when :show
342
+ :find
343
+ else
344
+ action
345
+ end
346
+ end
315
347
  end
316
348
  end
@@ -6,7 +6,7 @@ module Graphiti
6
6
  end
7
7
 
8
8
  generators do
9
- Dir[File.expand_path("../../deprecated_generators/**/*.rb", __dir__)].each do |f|
9
+ Dir[File.expand_path("../../deprecated_generators/**/*.rb", __dir__)].sort.each do |f|
10
10
  require f
11
11
  end
12
12
  end
@@ -110,10 +110,10 @@ module Graphiti
110
110
  end
111
111
 
112
112
  route = begin
113
- ::Rails.application.routes.recognize_path(path, method: method)
114
- rescue
115
- nil
116
- end
113
+ ::Rails.application.routes.recognize_path(path, method: method)
114
+ rescue
115
+ nil
116
+ end
117
117
  "#{route[:controller]}_controller".classify.safe_constantize if route
118
118
  }
119
119
  end
@@ -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
@@ -1,23 +1,23 @@
1
1
  module Graphiti
2
2
  class RequestValidator
3
3
  delegate :validate,
4
- :validate!,
5
- :errors,
6
- :deserialized_payload,
7
- to: :@validator
4
+ :validate!,
5
+ :errors,
6
+ :deserialized_payload,
7
+ to: :@validator
8
8
 
9
- def initialize(root_resource, raw_params)
10
- @validator = ValidatorFactory.create(root_resource, raw_params)
9
+ def initialize(root_resource, raw_params, action)
10
+ @validator = ValidatorFactory.create(root_resource, raw_params, action)
11
11
  end
12
12
 
13
13
  class ValidatorFactory
14
- def self.create(root_resource, raw_params)
15
- case raw_params["action"]
16
- when "update" then
14
+ def self.create(root_resource, raw_params, action)
15
+ case action
16
+ when :update
17
17
  RequestValidators::UpdateValidator
18
18
  else
19
19
  RequestValidators::Validator
20
- end.new(root_resource, raw_params)
20
+ end.new(root_resource, raw_params, action)
21
21
  end
22
22
  end
23
23
  end
@@ -5,7 +5,7 @@ module Graphiti
5
5
  if required_payload? && payload_matches_endpoint?
6
6
  super
7
7
  else
8
- return false
8
+ false
9
9
  end
10
10
  end
11
11
 
@@ -26,18 +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
-
40
- meta_type = @raw_params.dig(:data, :type)
39
+ meta_type = @params.dig(:data, :type)
41
40
 
42
41
  # NOTE: calling #to_s and comparing 2 strings is slower than
43
42
  # calling #to_sym and comparing 2 symbols. But pre ruby-2.2
@@ -3,43 +3,47 @@ module Graphiti
3
3
  class Validator
4
4
  attr_reader :errors
5
5
 
6
- def initialize(root_resource, raw_params)
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
+ @action = action
10
11
  end
11
12
 
12
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
+
13
19
  resource = @root_resource
14
- if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
15
- if @root_resource.type != meta_type && @root_resource.polymorphic?
16
- 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
17
26
  end
18
- end
19
27
 
20
- typecast_attributes(resource, deserialized_payload.attributes, deserialized_payload.meta[:payload_path])
21
- 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
22
33
 
23
34
  errors.blank?
24
35
  end
25
36
 
26
37
  def validate!
27
38
  unless validate
28
- raise @error_class || Graphiti::Errors::InvalidRequest, self.errors
39
+ raise @error_class || Graphiti::Errors::InvalidRequest, errors
29
40
  end
30
41
 
31
42
  true
32
43
  end
33
44
 
34
45
  def deserialized_payload
35
- @deserialized_payload ||= begin
36
- payload = normalized_params
37
- if payload[:data] && payload[:data][:type]
38
- Graphiti::Deserializer.new(payload)
39
- else
40
- Graphiti::Deserializer.new({})
41
- end
42
- end
46
+ @deserialized_payload ||= Graphiti::Deserializer.new(@params)
43
47
  end
44
48
 
45
49
  private
@@ -47,10 +51,10 @@ module Graphiti
47
51
  def process_relationships(resource, relationships, payload_path)
48
52
  opts = {
49
53
  resource: resource,
50
- relationships: relationships,
54
+ relationships: relationships
51
55
  }
52
56
 
53
- Graphiti::Util::RelationshipPayload.iterate(opts) do |x|
57
+ Graphiti::Util::RelationshipPayload.iterate(**opts) do |x|
54
58
  sideload_def = x[:sideload]
55
59
 
56
60
  unless sideload_def.writable?
@@ -61,13 +65,23 @@ module Graphiti
61
65
  next
62
66
  end
63
67
 
64
- typecast_attributes(x[:resource], x[:attributes], x[:meta][:payload_path])
65
- 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)
66
75
  end
67
76
  end
68
77
 
69
- def typecast_attributes(resource, attributes, payload_path)
78
+ def typecast_attributes(resource, attributes, action, payload_path)
70
79
  attributes.each_pair do |key, value|
80
+ # Only validate id if create action, otherwise it's only used for lookup
81
+ next if action != :create &&
82
+ key == :id &&
83
+ resource.class.config[:attributes][:id][:writable] == false
84
+
71
85
  begin
72
86
  attributes[key] = resource.typecast(key, value, :writable)
73
87
  rescue Graphiti::Errors::UnknownAttribute
@@ -80,8 +94,8 @@ module Graphiti
80
94
  end
81
95
  end
82
96
 
83
- def normalized_params
84
- normalized = @raw_params
97
+ def normalized_params(raw_params)
98
+ normalized = raw_params
85
99
  if normalized.respond_to?(:to_unsafe_h)
86
100
  normalized = normalized.to_unsafe_h.deep_symbolize_keys
87
101
  end
@@ -22,12 +22,20 @@ module Graphiti
22
22
  end
23
23
 
24
24
  def type=(val)
25
- val = val && val.to_sym
25
+ val = val&.to_sym
26
26
  if (val = super)
27
27
  serializer.type(val)
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)
@@ -42,7 +50,7 @@ module Graphiti
42
50
  path: val,
43
51
  full_path: val,
44
52
  url: val,
45
- actions: [:index, :show],
53
+ actions: [:index, :show]
46
54
  }
47
55
  end
48
56
 
@@ -82,7 +90,10 @@ module Graphiti
82
90
  :attributes_schema_by_default,
83
91
  :relationships_readable_by_default,
84
92
  :relationships_writable_by_default,
85
- :filters_accept_nil_by_default
93
+ :filters_accept_nil_by_default,
94
+ :filters_deny_empty_by_default,
95
+ :graphql_entrypoint,
96
+ :cursor_paginatable
86
97
 
87
98
  class << self
88
99
  prepend Overrides
@@ -96,6 +107,7 @@ module Graphiti
96
107
  # re-assigning causes a new Class.new
97
108
  klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
98
109
  klass.type ||= klass.infer_type
110
+ klass.graphql_entrypoint = klass.type.to_s.pluralize.to_sym
99
111
  default(klass, :attributes_readable_by_default, true)
100
112
  default(klass, :attributes_writable_by_default, true)
101
113
  default(klass, :attributes_sortable_by_default, true)
@@ -104,6 +116,7 @@ module Graphiti
104
116
  default(klass, :relationships_readable_by_default, true)
105
117
  default(klass, :relationships_writable_by_default, true)
106
118
  default(klass, :filters_accept_nil_by_default, false)
119
+ default(klass, :filters_deny_empty_by_default, false)
107
120
 
108
121
  unless klass.config[:attributes][:id]
109
122
  klass.attribute :id, :integer_id
@@ -127,7 +140,7 @@ module Graphiti
127
140
  def get_attr(name, flag, opts = {})
128
141
  defaults = {request: false}
129
142
  opts = defaults.merge(opts)
130
- new.get_attr(name, flag, opts)
143
+ new.get_attr(name, flag, **opts)
131
144
  end
132
145
 
133
146
  def abstract_class?
@@ -142,19 +155,20 @@ module Graphiti
142
155
  if (@abstract_class = val)
143
156
  self.serializer = nil
144
157
  self.type = nil
158
+ self.graphql_entrypoint = nil
145
159
  end
146
160
  end
147
161
 
148
162
  def infer_type
149
163
  if name.present?
150
- name.demodulize.gsub("Resource", "").underscore.pluralize.to_sym
164
+ name.demodulize.sub(/.*\KResource/, "").underscore.pluralize.to_sym
151
165
  else
152
166
  :undefined_jsonapi_type
153
167
  end
154
168
  end
155
169
 
156
170
  def infer_model
157
- name&.gsub("Resource", "")&.safe_constantize
171
+ name&.sub(/.*\KResource/, "")&.safe_constantize
158
172
  end
159
173
 
160
174
  # @api private
@@ -186,6 +200,7 @@ module Graphiti
186
200
  @config ||=
187
201
  {
188
202
  filters: {},
203
+ grouped_filters: {},
189
204
  default_filters: {},
190
205
  stats: {},
191
206
  sort_all: nil,
@@ -198,6 +213,7 @@ module Graphiti
198
213
  extra_attributes: {},
199
214
  sideloads: {},
200
215
  callbacks: {},
216
+ links: {}
201
217
  }
202
218
  end
203
219
 
@@ -221,6 +237,10 @@ module Graphiti
221
237
  config[:filters]
222
238
  end
223
239
 
240
+ def grouped_filters
241
+ config[:grouped_filters]
242
+ end
243
+
224
244
  def sorts
225
245
  config[:sorts]
226
246
  end
@@ -236,11 +256,15 @@ module Graphiti
236
256
  def default_filters
237
257
  config[:default_filters]
238
258
  end
259
+
260
+ def links
261
+ config[:links]
262
+ end
239
263
  end
240
264
 
241
265
  def get_attr!(name, flag, options = {})
242
266
  options[:raise_error] = true
243
- get_attr(name, flag, options)
267
+ get_attr(name, flag, **options)
244
268
  end
245
269
 
246
270
  def get_attr(name, flag, request: false, raise_error: false)
@@ -255,6 +279,10 @@ module Graphiti
255
279
  self.class.filters
256
280
  end
257
281
 
282
+ def grouped_filters
283
+ self.class.grouped_filters
284
+ end
285
+
258
286
  def sort_all
259
287
  self.class.sort_all
260
288
  end