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

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