graphiti 1.0.alpha.5 → 1.0.alpha.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphiti/api_test_generator.rb +71 -0
  3. data/lib/generators/graphiti/generator_mixin.rb +45 -0
  4. data/lib/generators/graphiti/resource_generator.rb +99 -0
  5. data/lib/generators/graphiti/resource_test_generator.rb +57 -0
  6. data/lib/generators/{jsonapi → graphiti}/templates/application_resource.rb.erb +0 -0
  7. data/lib/generators/{jsonapi → graphiti}/templates/controller.rb.erb +0 -0
  8. data/lib/generators/{jsonapi → graphiti}/templates/create_request_spec.rb.erb +3 -3
  9. data/lib/generators/{jsonapi → graphiti}/templates/destroy_request_spec.rb.erb +5 -5
  10. data/lib/generators/graphiti/templates/index_request_spec.rb.erb +22 -0
  11. data/lib/generators/{jsonapi → graphiti}/templates/resource.rb.erb +0 -0
  12. data/lib/generators/{jsonapi → graphiti}/templates/resource_reads_spec.rb.erb +12 -12
  13. data/lib/generators/{jsonapi → graphiti}/templates/resource_writes_spec.rb.erb +12 -12
  14. data/lib/generators/graphiti/templates/show_request_spec.rb.erb +21 -0
  15. data/lib/generators/{jsonapi → graphiti}/templates/update_request_spec.rb.erb +5 -5
  16. data/lib/graphiti.rb +0 -6
  17. data/lib/graphiti/adapters/abstract.rb +0 -1
  18. data/lib/graphiti/adapters/active_record/inferrence.rb +8 -1
  19. data/lib/graphiti/base.rb +1 -1
  20. data/lib/graphiti/errors.rb +70 -5
  21. data/lib/graphiti/jsonapi_serializable_ext.rb +18 -6
  22. data/lib/graphiti/query.rb +28 -17
  23. data/lib/graphiti/resource.rb +14 -0
  24. data/lib/graphiti/resource/configuration.rb +22 -3
  25. data/lib/graphiti/resource/dsl.rb +17 -2
  26. data/lib/graphiti/resource/interface.rb +10 -8
  27. data/lib/graphiti/resource/links.rb +6 -3
  28. data/lib/graphiti/runner.rb +2 -1
  29. data/lib/graphiti/schema.rb +33 -5
  30. data/lib/graphiti/schema_diff.rb +71 -1
  31. data/lib/graphiti/scope.rb +4 -9
  32. data/lib/graphiti/scoping/base.rb +2 -2
  33. data/lib/graphiti/scoping/filter.rb +5 -0
  34. data/lib/graphiti/scoping/filterable.rb +21 -6
  35. data/lib/graphiti/scoping/paginate.rb +4 -4
  36. data/lib/graphiti/scoping/sort.rb +26 -5
  37. data/lib/graphiti/sideload.rb +13 -22
  38. data/lib/graphiti/sideload/belongs_to.rb +11 -2
  39. data/lib/graphiti/sideload/has_many.rb +1 -1
  40. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +2 -3
  41. data/lib/graphiti/types.rb +1 -1
  42. data/lib/graphiti/util/class.rb +5 -2
  43. data/lib/graphiti/util/persistence.rb +1 -1
  44. data/lib/graphiti/version.rb +1 -1
  45. metadata +16 -13
  46. data/lib/generators/jsonapi/resource_generator.rb +0 -169
  47. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
  48. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -21
@@ -0,0 +1,21 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "<%= type %>#show", type: :request do
4
+ let(:params) { {} }
5
+
6
+ subject(:make_request) do
7
+ jsonapi_get "/<%= api_namespace %>/v1/<%= type %>/#{<%= var %>.id}", params: params
8
+ end
9
+
10
+ describe 'basic fetch' do
11
+ let!(:<%= var %>) { create(:<%= var %>) }
12
+
13
+ it 'works' do
14
+ expect(<%= resource_class %>).to receive(:find).and_call_original
15
+ make_request
16
+ expect(response.status).to eq(200)
17
+ expect(d.jsonapi_type).to eq('<%= type %>')
18
+ expect(d.id).to eq(<%= var %>.id)
19
+ end
20
+ end
21
+ end
@@ -2,16 +2,16 @@ require 'rails_helper'
2
2
 
3
3
  RSpec.describe "<%= type %>#update", type: :request do
4
4
  subject(:make_request) do
5
- jsonapi_put "/<%= api_namespace %>/v1/<%= type %>/#{<%= file_name %>.id}", payload
5
+ jsonapi_put "/<%= api_namespace %>/v1/<%= type %>/#{<%= var %>.id}", payload
6
6
  end
7
7
 
8
8
  describe 'basic update' do
9
- let!(:<%= file_name %>) { create(:<%= file_name %>) }
9
+ let!(:<%= var %>) { create(:<%= var %>) }
10
10
 
11
11
  let(:payload) do
12
12
  {
13
13
  data: {
14
- id: <%= file_name %>.id.to_s,
14
+ id: <%= var %>.id.to_s,
15
15
  type: '<%= type %>',
16
16
  attributes: {
17
17
  # ... your attrs here
@@ -22,10 +22,10 @@ RSpec.describe "<%= type %>#update", type: :request do
22
22
 
23
23
  # Replace 'xit' with 'it' after adding attributes
24
24
  xit 'updates the resource' do
25
- expect(<%= resource_klass %>).to receive(:find).and_call_original
25
+ expect(<%= resource_class %>).to receive(:find).and_call_original
26
26
  expect {
27
27
  make_request
28
- }.to change { <%= file_name %>.reload.attributes }
28
+ }.to change { <%= var %>.reload.attributes }
29
29
  expect(response.status).to eq(200)
30
30
 
31
31
  # ... assert updates attributes ...
data/lib/graphiti.rb CHANGED
@@ -126,12 +126,6 @@ module Graphiti
126
126
  def self.resources
127
127
  @resources ||= []
128
128
  end
129
-
130
- def self.check!
131
- resources.each do |resource|
132
- resource.sideloads.values.each(&:check!)
133
- end
134
- end
135
129
  end
136
130
 
137
131
  require "graphiti/runner"
@@ -475,7 +475,6 @@ module Graphiti
475
475
  def activerecord_associate?(parent, child, association_name)
476
476
  defined?(::ActiveRecord) &&
477
477
  parent.is_a?(::ActiveRecord::Base) &&
478
- child.is_a?(::ActiveRecord::Base) &&
479
478
  parent.class.reflect_on_association(association_name)
480
479
  end
481
480
  end
@@ -2,9 +2,16 @@ module Graphiti
2
2
  module Adapters
3
3
  module ActiveRecord
4
4
  module Inferrence
5
+ # If going AR to AR, use AR introspection
6
+ # If going AR to PORO, fall back to normal inferrence
5
7
  def infer_foreign_key
6
8
  parent_model = parent_resource_class.model
7
- parent_model.reflections[association_name.to_s].foreign_key.to_sym
9
+ reflection = parent_model.reflections[association_name.to_s]
10
+ if reflection
11
+ reflection.foreign_key.to_sym
12
+ else
13
+ super
14
+ end
8
15
  end
9
16
  end
10
17
  end
data/lib/graphiti/base.rb CHANGED
@@ -11,7 +11,7 @@ module Graphiti
11
11
  end
12
12
 
13
13
  def query_hash
14
- @query_hash ||= query.to_hash
14
+ @query_hash ||= query.hash
15
15
  end
16
16
 
17
17
  def wrap_context
@@ -16,6 +16,53 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
16
16
  end
17
17
  end
18
18
 
19
+ class SingularSideload < Base
20
+ def initialize(sideload, parent_length)
21
+ @sideload = sideload
22
+ @parent_length = parent_length
23
+ end
24
+
25
+ def message
26
+ <<-MSG
27
+ #{@sideload.parent_resource.class.name}: tried to sideload #{@sideload.name.inspect}, but more than one #{@sideload.parent_resource.model.name} was passed!
28
+
29
+ This is because you marked the sideload #{@sideload.name.inspect} with single: true
30
+
31
+ You might have done this because the sideload can only be loaded from a :show endpoint, and :index would be too expensive.
32
+
33
+ Remove the single: true option to bypass this error.
34
+ MSG
35
+ end
36
+ end
37
+
38
+ class UnsupportedSort < Base
39
+ def initialize(resource, attribute, whitelist, direction)
40
+ @resource = resource
41
+ @attribute = attribute
42
+ @whitelist = whitelist
43
+ @direction = direction
44
+ end
45
+
46
+ def message
47
+ <<-MSG
48
+ #{@resource.class.name}: tried to sort on attribute #{@attribute.inspect}, but passed #{@direction.inspect} when only #{@whitelist.inspect} is supported.
49
+ MSG
50
+ end
51
+ end
52
+
53
+ class ExtraAttributeNotFound < Base
54
+ def initialize(resource_class, name)
55
+ @resource_class = resource_class
56
+ @name = name
57
+ end
58
+
59
+ def message
60
+ <<-MSG
61
+ #{@resource_class.name}: called .on_extra_attribute #{@name.inspect}, but extra attribute #{@name.inspect} does not exist!
62
+ MSG
63
+ end
64
+ end
65
+
19
66
  class InvalidFilterValue < Base
20
67
  def initialize(resource, filter, value)
21
68
  @resource = resource
@@ -158,7 +205,7 @@ Graphiti.config.context_for_endpoint = ->(path, action) { ... }
158
205
 
159
206
  Either set a primary endpoint for this resource:
160
207
 
161
- endpoint '/my/url', [:index, :show, :create]
208
+ primary_endpoint '/my/url', [:index, :show, :create]
162
209
 
163
210
  Or whitelist a secondary endpoint:
164
211
 
@@ -347,16 +394,34 @@ Expecting filter #{@filter.inspect} on #{@sideload.resource.class.name}.
347
394
  end
348
395
  end
349
396
 
397
+ class MissingDependentFilter < Base
398
+ def initialize(resource, filters)
399
+ @resource = resource
400
+ @filters = filters
401
+ end
402
+
403
+ def message
404
+ msg = "#{@resource.class.name}: The following filters had dependencies that were not passed:"
405
+ @filters.each do |f|
406
+ msg << "\n#{f[:filter][:name].inspect} - dependent on #{f[:filter][:dependencies].inspect}, but #{f[:missing].inspect} not passed."
407
+ end
408
+ msg
409
+ end
410
+ end
411
+
350
412
  class ResourceNotFound < Base
351
- def initialize(resource_class, sideload_name)
413
+ def initialize(resource_class, sideload_name, tried)
352
414
  @resource_class = resource_class
353
415
  @sideload_name = sideload_name
416
+ @tried = tried
354
417
  end
355
418
 
356
419
  def message
357
420
  <<-MSG
358
421
  Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource_class.name}'!
359
422
 
423
+ Tried to find classes: #{@tried.join(', ')}
424
+
360
425
  If this follows a non-standard naming convention, use the :resource option to pass it directly:
361
426
 
362
427
  has_many :comments, resource: SpecialCommentResource
@@ -395,13 +460,13 @@ Consider using a named relationship instead, e.g. 'has_one :top_comment'
395
460
  end
396
461
 
397
462
  class InvalidInclude < Base
398
- def initialize(relationship, parent_resource)
463
+ def initialize(resource_class, relationship)
464
+ @resource_class = resource_class
399
465
  @relationship = relationship
400
- @parent_resource = parent_resource
401
466
  end
402
467
 
403
468
  def message
404
- "The requested included relationship \"#{@relationship}\" is not supported on resource \"#{@parent_resource}\""
469
+ "#{@resource_class.name}: The requested included relationship \"#{@relationship}\" is not supported."
405
470
  end
406
471
  end
407
472
 
@@ -7,8 +7,9 @@ module Graphiti
7
7
  # To ensure we always render with the *resource* serializer
8
8
  module RendererOverrides
9
9
  def _build(object, exposures, klass)
10
- klass = object.instance_variable_get(:@__serializer_klass)
11
- klass.new(exposures.merge(object: object))
10
+ resource = object.instance_variable_get(:@__graphiti_resource)
11
+ klass = object.instance_variable_get(:@__graphiti_serializer)
12
+ klass.new(exposures.merge(object: object, resource: resource))
12
13
  end
13
14
  end
14
15
 
@@ -21,12 +22,14 @@ module Graphiti
21
22
  nil
22
23
  elsif resources.respond_to?(:to_ary)
23
24
  Array(resources).map do |obj|
24
- klass = obj.instance_variable_get(:@__serializer_klass)
25
- klass.new(@_exposures.merge(object: obj))
25
+ klass = obj.instance_variable_get(:@__graphiti_serializer)
26
+ resource = obj.instance_variable_get(:@__graphiti_resource)
27
+ klass.new(@_exposures.merge(object: obj, resource: resource))
26
28
  end
27
29
  else
28
- klass = resources.instance_variable_get(:@__serializer_klass)
29
- klass.new(@_exposures.merge(object: resources))
30
+ klass = resources.instance_variable_get(:@__graphiti_serializer)
31
+ resource = resources.instance_variable_get(:@__graphiti_resource)
32
+ klass.new(@_exposures.merge(object: resources, resource: resource))
30
33
  end
31
34
  end
32
35
  end
@@ -38,6 +41,15 @@ module Graphiti
38
41
  def requested_relationships(fields)
39
42
  @_relationships
40
43
  end
44
+
45
+ # Allow access to resource methods
46
+ def method_missing(id, *args, &blk)
47
+ if @resource.respond_to?(id, true)
48
+ @resource.send(id, *args, &blk)
49
+ else
50
+ super
51
+ end
52
+ end
41
53
  end
42
54
 
43
55
  JSONAPI::Serializable::Resource
@@ -1,6 +1,6 @@
1
1
  module Graphiti
2
2
  class Query
3
- attr_reader :resource, :include_hash, :association_name
3
+ attr_reader :resource, :include_hash, :association_name, :params
4
4
 
5
5
  def initialize(resource, params, association_name = nil, nested_include = nil, parents = [])
6
6
  @resource = resource
@@ -29,17 +29,17 @@ module Graphiti
29
29
  end
30
30
  end
31
31
 
32
- def to_hash
33
- {}.tap do |hash|
34
- hash[:filter] = filters unless filters.empty?
35
- hash[:sort] = sorts unless sorts.empty?
36
- hash[:page] = pagination unless pagination.empty?
32
+ def hash
33
+ @hash ||= {}.tap do |h|
34
+ h[:filter] = filters unless filters.empty?
35
+ h[:sort] = sorts unless sorts.empty?
36
+ h[:page] = pagination unless pagination.empty?
37
37
  unless association?
38
- hash[:fields] = fields unless fields.empty?
39
- hash[:extra_fields] = extra_fields unless extra_fields.empty?
38
+ h[:fields] = fields unless fields.empty?
39
+ h[:extra_fields] = extra_fields unless extra_fields.empty?
40
40
  end
41
- hash[:stats] = stats unless stats.empty?
42
- hash[:include] = sideload_hash unless sideload_hash.empty?
41
+ h[:stats] = stats unless stats.empty?
42
+ h[:include] = sideload_hash unless sideload_hash.empty?
43
43
  end
44
44
  end
45
45
 
@@ -53,7 +53,7 @@ module Graphiti
53
53
  @sideload_hash = begin
54
54
  {}.tap do |hash|
55
55
  sideloads.each_pair do |key, value|
56
- hash[key] = sideloads[key].to_hash
56
+ hash[key] = sideloads[key].hash
57
57
  end
58
58
  end
59
59
  end
@@ -66,6 +66,7 @@ module Graphiti
66
66
  sideload = @resource.class.sideload(key)
67
67
  if sideload
68
68
  _parents = parents + [self]
69
+ sub_hash = sub_hash[:include] if sub_hash.has_key?(:include)
69
70
  hash[key] = Query.new(sideload.resource, @params, key, sub_hash, _parents)
70
71
  else
71
72
  handle_missing_sideload(key)
@@ -163,6 +164,8 @@ module Graphiti
163
164
 
164
165
  whitelist ? Util::IncludeParams.scrub(requested, whitelist) : requested
165
166
  end
167
+
168
+ @include_hash
166
169
  end
167
170
 
168
171
  def stats
@@ -182,17 +185,25 @@ module Graphiti
182
185
 
183
186
  private
184
187
 
188
+ # Try to find on this resource
189
+ # If not there, follow the legacy logic of scalling all other
190
+ # resource names/types
191
+ # TODO: Eventually, remove the legacy logic
185
192
  def validate!(name, flag)
186
193
  return false if name.to_s.include?('.') # nested
187
194
 
188
- not_associated_name = !@resource.class.association_names.include?(name)
189
- not_associated_type = !@resource.class.association_types.include?(name)
195
+ if att = @resource.get_attr(name, flag, request: true)
196
+ return att
197
+ else
198
+ not_associated_name = !@resource.class.association_names.include?(name)
199
+ not_associated_type = !@resource.class.association_types.include?(name)
190
200
 
191
- if not_associated_name && not_associated_type
192
- @resource.get_attr!(name, flag, request: true)
193
- return true
201
+ if not_associated_name && not_associated_type
202
+ @resource.get_attr!(name, flag, request: true)
203
+ return true
204
+ end
205
+ false
194
206
  end
195
- false
196
207
  end
197
208
 
198
209
  def nested?(name)
@@ -9,6 +9,14 @@ module Graphiti
9
9
  attr_reader :context
10
10
 
11
11
  def around_scoping(scope, query_hash)
12
+ extra_fields = query_hash[:extra_fields] || {}
13
+ extra_fields = extra_fields[type] || []
14
+ extra_fields.each do |name|
15
+ if config = self.class.config[:extra_attributes][name]
16
+ scope = instance_exec(scope, &config[:hook]) if config[:hook]
17
+ end
18
+ end
19
+
12
20
  yield scope
13
21
  end
14
22
 
@@ -16,6 +24,12 @@ module Graphiti
16
24
  serializer
17
25
  end
18
26
 
27
+ def decorate_record(record)
28
+ serializer = serializer_for(record)
29
+ record.instance_variable_set(:@__graphiti_serializer, serializer)
30
+ record.instance_variable_set(:@__graphiti_resource, self)
31
+ end
32
+
19
33
  def with_context(object, namespace = nil)
20
34
  Graphiti.with_context(object, namespace) do
21
35
  yield
@@ -3,6 +3,8 @@ module Graphiti
3
3
  module Configuration
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ DEFAULT_MAX_PAGE_SIZE = 1_000
7
+
6
8
  module Overrides
7
9
  def serializer=(val)
8
10
  if val
@@ -52,6 +54,7 @@ module Graphiti
52
54
  :serializer,
53
55
  :default_page_size,
54
56
  :default_sort,
57
+ :max_page_size,
55
58
  :attributes_readable_by_default,
56
59
  :attributes_writable_by_default,
57
60
  :attributes_sortable_by_default,
@@ -67,13 +70,12 @@ module Graphiti
67
70
  super
68
71
  klass.config = Util::Hash.deep_dup(config)
69
72
  klass.adapter ||= Adapters::Abstract
70
- klass.default_sort ||= []
71
- klass.default_page_size ||= 20
73
+ klass.max_page_size ||= DEFAULT_MAX_PAGE_SIZE
72
74
  # re-assigning causes a new Class.new
73
75
  if klass.serializer
74
76
  klass.serializer = klass.serializer
75
77
  else
76
- klass.serializer = JSONAPI::Serializable::Resource
78
+ klass.serializer = klass.infer_serializer_superclass
77
79
  end
78
80
  klass.type ||= klass.infer_type
79
81
  default(klass, :attributes_readable_by_default, true)
@@ -130,6 +132,23 @@ module Graphiti
130
132
  name.gsub('Resource', '').safe_constantize if name
131
133
  end
132
134
 
135
+ # @api private
136
+ def infer_serializer_superclass
137
+ serializer_class = JSONAPI::Serializable::Resource
138
+ namespace = Util::Class.namespace_for(self)
139
+ app_serializer = "#{namespace}::ApplicationSerializer"
140
+ .safe_constantize
141
+ app_serializer ||= "ApplicationSerializer".safe_constantize
142
+
143
+ if app_serializer
144
+ if app_serializer.ancestors.include?(serializer_class)
145
+ serializer_class = app_serializer
146
+ end
147
+ end
148
+
149
+ serializer_class
150
+ end
151
+
133
152
  def default(object, attr, value)
134
153
  prior = object.send(attr)
135
154
  unless prior || prior == false
@@ -15,12 +15,16 @@ module Graphiti
15
15
  opts[:single] = true
16
16
  end
17
17
 
18
+ required = att[:filterable] == :required || !!opts[:required]
18
19
  config[:filters][name.to_sym] = {
19
20
  aliases: aliases,
21
+ name: name.to_sym,
20
22
  type: att[:type],
21
23
  allow: opts[:allow],
22
24
  reject: opts[:reject],
23
25
  single: !!opts[:single],
26
+ dependencies: opts[:dependent],
27
+ required: required,
24
28
  operators: operators.to_hash
25
29
  }
26
30
  else
@@ -45,7 +49,9 @@ module Graphiti
45
49
  opts = args.extract_options!
46
50
 
47
51
  if get_attr(name, :sortable, raise_error: :only_unsupported)
48
- config[:sorts][name] = blk
52
+ config[:sorts][name] = {
53
+ proc: blk
54
+ }.merge(opts.slice(:only))
49
55
  else
50
56
  if type = args[0]
51
57
  attribute name, type, only: [:sortable]
@@ -89,7 +95,8 @@ module Graphiti
89
95
  options[:proc] = blk
90
96
  config[:attributes][name] = options
91
97
  apply_attributes_to_serializer
92
- filter(name) if options[:filterable]
98
+ options[:filterable] ? filter(name) : config[:filters].delete(name)
99
+ options[:sortable] ? sort(name) : config[:sorts].delete(name)
93
100
  end
94
101
 
95
102
  def extra_attribute(name, type, options = {}, &blk)
@@ -107,6 +114,14 @@ module Graphiti
107
114
  apply_extra_attributes_to_serializer
108
115
  end
109
116
 
117
+ def on_extra_attribute(name, &blk)
118
+ if config[:extra_attributes][name]
119
+ config[:extra_attributes][name][:hook] = blk
120
+ else
121
+ raise Errors::ExtraAttributeNotFound.new(self, name)
122
+ end
123
+ end
124
+
110
125
  def all_attributes
111
126
  attributes.merge(extra_attributes)
112
127
  end