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
@@ -5,38 +5,40 @@ module Graphiti
5
5
 
6
6
  class_methods do
7
7
  def all(params = {}, base_scope = nil)
8
- validate!
8
+ validate!(params)
9
9
  _all(params, {}, base_scope)
10
10
  end
11
11
 
12
12
  # @api private
13
13
  def _all(params, opts, base_scope)
14
- runner = Runner.new(self, params)
14
+ runner = Runner.new(self, params, opts.delete(:query))
15
15
  runner.proxy(base_scope, opts)
16
16
  end
17
17
 
18
- def find(params, base_scope = nil)
19
- validate!
18
+ def find(params = {}, base_scope = nil)
19
+ validate!(params)
20
20
  id = params[:data].try(:[], :id) || params.delete(:id)
21
21
  params[:filter] ||= {}
22
- params[:filter].merge!(id: id)
22
+ params[:filter].merge!(id: id) if id
23
23
 
24
24
  runner = Runner.new(self, params)
25
25
  runner.proxy(base_scope, single: true, raise_on_missing: true)
26
26
  end
27
27
 
28
28
  def build(params, base_scope = nil)
29
- validate!
29
+ validate!(params)
30
30
  runner = Runner.new(self, params)
31
31
  runner.proxy(base_scope, single: true, raise_on_missing: true)
32
32
  end
33
33
 
34
34
  private
35
35
 
36
- def validate!
36
+ def validate!(params)
37
+ return unless validate_endpoints?
38
+
37
39
  if context && context.respond_to?(:request)
38
40
  path = context.request.env['PATH_INFO']
39
- unless allow_request?(path, context_namespace)
41
+ unless allow_request?(path, params, context_namespace)
40
42
  raise Errors::InvalidEndpoint.new(self, path, context_namespace)
41
43
  end
42
44
  end
@@ -19,9 +19,11 @@ module Graphiti
19
19
  :base_url,
20
20
  :endpoint_namespace,
21
21
  :secondary_endpoints,
22
- :autolink
22
+ :autolink,
23
+ :validate_endpoints
23
24
  self.secondary_endpoints = []
24
25
  self.autolink = true
26
+ self.validate_endpoints = true
25
27
 
26
28
  class << self
27
29
  prepend Overrides
@@ -66,9 +68,10 @@ module Graphiti
66
68
  ([endpoint] + secondary_endpoints).compact
67
69
  end
68
70
 
69
- def allow_request?(path, action)
71
+ def allow_request?(path, params, action)
70
72
  endpoints.any? do |e|
71
- if [:update, :show, :destroy].include?(context_namespace)
73
+ has_id = params[:id] || params[:data].try(:[], :id)
74
+ if [:update, :show, :destroy].include?(context_namespace) && has_id
72
75
  path = path.split('/')
73
76
  path.pop
74
77
  path = path.join('/')
@@ -3,9 +3,10 @@ module Graphiti
3
3
  attr_reader :params
4
4
  include Graphiti::Base
5
5
 
6
- def initialize(resource_class, params)
6
+ def initialize(resource_class, params, query = nil)
7
7
  @resource_class = resource_class
8
8
  @params = params
9
+ @query = query
9
10
  end
10
11
 
11
12
  def jsonapi_resource
@@ -84,10 +84,19 @@ module Graphiti
84
84
  type: r.type.to_s,
85
85
  attributes: attributes(r),
86
86
  extra_attributes: extra_attributes(r),
87
+ sorts: sorts(r),
87
88
  filters: filters(r),
88
89
  relationships: relationships(r)
89
90
  }
90
91
 
92
+ if r.default_sort
93
+ config[:default_sort] = r.default_sort
94
+ end
95
+
96
+ if r.default_page_size
97
+ config[:default_page_size] = r.default_page_size
98
+ end
99
+
91
100
  if r.polymorphic?
92
101
  config.merge!(polymorphic: true, children: r.children.map(&:name))
93
102
  end
@@ -99,12 +108,11 @@ module Graphiti
99
108
  def attributes(resource)
100
109
  {}.tap do |attrs|
101
110
  resource.attributes.each_pair do |name, config|
102
- if config.values_at(:readable, :writable, :sortable).any?
111
+ if config.values_at(:readable, :writable).any?
103
112
  attrs[name] = {
104
113
  type: config[:type].to_s,
105
114
  readable: flag(config[:readable]),
106
- writable: flag(config[:writable]),
107
- sortable: flag(config[:sortable])
115
+ writable: flag(config[:writable])
108
116
  }
109
117
  end
110
118
  end
@@ -130,6 +138,20 @@ module Graphiti
130
138
  end
131
139
  end
132
140
 
141
+ def sorts(resource)
142
+ {}.tap do |s|
143
+ resource.sorts.each_pair do |name, sort|
144
+ config = {}
145
+ config[:only] = sort[:only] if sort[:only]
146
+ attr = resource.attributes[name]
147
+ if attr[:sortable].is_a?(Symbol)
148
+ config[:guard] = true
149
+ end
150
+ s[name] = config
151
+ end
152
+ end
153
+ end
154
+
133
155
  def filters(resource)
134
156
  {}.tap do |f|
135
157
  resource.filters.each_pair do |name, filter|
@@ -137,9 +159,11 @@ module Graphiti
137
159
  type: filter[:type].to_s,
138
160
  operators: filter[:operators].keys.map(&:to_s)
139
161
  }
162
+
140
163
  config[:single] = true if filter[:single]
141
- config[:allow] = filter[:allow] if filter[:allow]
142
- config[:reject] = filter[:reject] if filter[:reject]
164
+ config[:allow] = filter[:allow].map(&:to_s) if filter[:allow]
165
+ config[:reject] = filter[:reject].map(&:to_s) if filter[:reject]
166
+ config[:dependencies] = filter[:dependencies].map(&:to_s) if filter[:dependencies]
143
167
 
144
168
  attr = resource.attributes[name]
145
169
  if attr[:filterable].is_a?(Symbol)
@@ -165,6 +189,10 @@ module Graphiti
165
189
  schema[:resource] = config.resource.class.name
166
190
  end
167
191
 
192
+ if config.single?
193
+ schema[:single] = true
194
+ end
195
+
168
196
  r[name] = schema
169
197
  end
170
198
  end
@@ -24,7 +24,9 @@ module Graphiti
24
24
  new_resource = @new[:resources].find { |n| n[:name] == r[:name] }
25
25
  compare_resource(r, new_resource) do
26
26
  compare_attributes(r, new_resource)
27
+ compare_defaults(r, new_resource)
27
28
  compare_extra_attributes(r, new_resource)
29
+ compare_sorts(r, new_resource)
28
30
  compare_filters(r, new_resource)
29
31
  compare_relationships(r, new_resource)
30
32
  end
@@ -54,6 +56,36 @@ module Graphiti
54
56
  end
55
57
  end
56
58
 
59
+ def compare_defaults(old_resource, new_resource)
60
+ if new_resource[:default_sort] && !old_resource[:default_sort]
61
+ @errors << "#{old_resource[:name]}: default sort added."
62
+ end
63
+
64
+ if old_resource[:default_sort] && !new_resource[:default_sort]
65
+ @errors << "#{old_resource[:name]}: default sort removed."
66
+ end
67
+
68
+ if new_resource[:default_sort] && old_resource[:default_sort]
69
+ if new_resource[:default_sort] != old_resource[:default_sort]
70
+ @errors << "#{old_resource[:name]}: default sort changed from #{old_resource[:default_sort].inspect} to #{new_resource[:default_sort].inspect}."
71
+ end
72
+ end
73
+
74
+ if new_resource[:default_page_size] && !old_resource[:default_page_size]
75
+ @errors << "#{old_resource[:name]}: default page size added."
76
+ end
77
+
78
+ if old_resource[:default_page_size] && !new_resource[:default_page_size]
79
+ @errors << "#{old_resource[:name]}: default page size removed."
80
+ end
81
+
82
+ if old_resource[:default_page_size] && new_resource[:default_page_size]
83
+ if old_resource[:default_page_size] != new_resource[:default_page_size]
84
+ @errors << "#{old_resource[:name]}: default page size changed from #{old_resource[:default_page_size]} to #{new_resource[:default_page_size]}."
85
+ end
86
+ end
87
+ end
88
+
57
89
  def compare_relationships(old_resource, new_resource)
58
90
  old_resource[:relationships].each_pair do |name, old_rel|
59
91
  unless new_rel = new_resource[:relationships][name]
@@ -61,6 +93,11 @@ module Graphiti
61
93
  next
62
94
  end
63
95
 
96
+ if new_rel[:single] && !old_rel[:single]
97
+ @errors << "#{old_resource[:name]}: relationship #{name.inspect} became single: true."
98
+ next
99
+ end
100
+
64
101
  if new_rel[:resource] != old_rel[:resource]
65
102
  @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}."
66
103
  end
@@ -82,6 +119,29 @@ module Graphiti
82
119
  end
83
120
  end
84
121
 
122
+ def compare_sorts(old_resource, new_resource)
123
+ old_resource[:sorts].each_pair do |name, old_sort|
124
+ unless new_sort = new_resource[:sorts][name]
125
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} was removed."
126
+ next
127
+ end
128
+
129
+ if new_sort[:guard] && !old_sort[:guard]
130
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} became guarded."
131
+ end
132
+
133
+ if new_sort[:only] && !old_sort[:only]
134
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} now limited to only #{new_sort[:only].inspect}."
135
+ end
136
+
137
+ if new_sort[:only] && old_sort[:only]
138
+ if new_sort[:only] != old_sort[:only]
139
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} was limited to only #{old_sort[:only].inspect}, now limited to only #{new_sort[:only].inspect}."
140
+ end
141
+ end
142
+ end
143
+ end
144
+
85
145
  def compare_filters(old_resource, new_resource)
86
146
  old_resource[:filters].each_pair do |name, old_filter|
87
147
  unless new_filter = new_resource[:filters][name]
@@ -98,6 +158,16 @@ module Graphiti
98
158
  @errors << "#{old_resource[:name]}: filter #{name.inspect} became singular."
99
159
  end
100
160
 
161
+ if new_filter[:dependencies] && !old_filter[:dependencies]
162
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} added dependencies #{new_filter[:dependencies].inspect}."
163
+ end
164
+
165
+ if new_filter[:dependencies] && old_filter[:dependencies]
166
+ if new_filter[:dependencies] != old_filter[:dependenices]
167
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} changed dependencies from #{old_filter[:dependencies].inspect} to #{new_filter[:dependencies].inspect}."
168
+ end
169
+ end
170
+
101
171
  if new_filter[:allow] != old_filter[:allow]
102
172
  new = new_filter[:allow] || []
103
173
  old = old_filter[:allow] || []
@@ -182,7 +252,7 @@ module Graphiti
182
252
  @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed type from #{old_att[:type].inspect} to #{new_att[:type].inspect}."
183
253
  end
184
254
 
185
- [:readable, :writable, :sortable].each do |flag|
255
+ [:readable, :writable].each do |flag|
186
256
  if [true, 'guarded'].include?(old_att[flag]) && new_att[flag] == false
187
257
  @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed flag #{flag.inspect} from #{old_att[flag].inspect} to #{new_att[flag].inspect}."
188
258
  end
@@ -8,13 +8,13 @@ module Graphiti
8
8
  @query = query
9
9
  @opts = opts
10
10
 
11
- @object = @resource.around_scoping(@object, query_hash) do |scope|
11
+ @object = @resource.around_scoping(@object, @query.hash) do |scope|
12
12
  apply_scoping(scope, opts)
13
13
  end
14
14
  end
15
15
 
16
16
  def resolve_stats
17
- if query_hash[:stats]
17
+ if @query.hash[:stats]
18
18
  Stats::Payload.new(@resource, @query, @unpaginated_object).generate
19
19
  else
20
20
  {}
@@ -36,18 +36,13 @@ module Graphiti
36
36
  end
37
37
  end
38
38
 
39
- def query_hash
40
- @query_hash ||= @query.to_hash
41
- end
42
-
43
39
  private
44
40
 
45
41
  # Used to ensure the resource's serializer is used
46
42
  # Not one derived through the usual jsonapi-rb logic
47
43
  def assign_serializer(records)
48
44
  records.each do |r|
49
- serializer = @resource.serializer_for(r)
50
- r.instance_variable_set(:@__serializer_klass, serializer)
45
+ @resource.decorate_record(r)
51
46
  end
52
47
  end
53
48
 
@@ -91,7 +86,7 @@ module Graphiti
91
86
  end
92
87
 
93
88
  def add_scoping(key, scoping_class, opts, default = {})
94
- @object = scoping_class.new(@resource, query_hash, @object, opts).apply
89
+ @object = scoping_class.new(@resource, @query.hash, @object, opts).apply
95
90
  @unpaginated_object = @object unless key == :paginate
96
91
  end
97
92
  end
@@ -6,7 +6,7 @@ module Graphiti
6
6
  # a default if not part of the user request.
7
7
  #
8
8
  # @attr_reader [Resource] resource The corresponding Resource instance
9
- # @attr_reader [Hash] query_hash the Query#to_hash node relevant to the current resource
9
+ # @attr_reader [Hash] query_hash the Query#hash node relevant to the current resource
10
10
  #
11
11
  # @see Scoping::DefaultFilter
12
12
  # @see Scoping::ExtraFields
@@ -15,7 +15,7 @@ module Graphiti
15
15
  # @see Scoping::Sort
16
16
  # @see Scope#initialize
17
17
  # @see Scope#query_hash
18
- # @see Query#to_hash
18
+ # @see Query#hash
19
19
  class Base
20
20
  attr_reader :resource, :query_hash
21
21
 
@@ -32,6 +32,11 @@ module Graphiti
32
32
  raise Errors::RequiredFilter.new(resource, missing_required_filters)
33
33
  end
34
34
 
35
+ if missing_dependent_filters.any?
36
+ raise Errors::MissingDependentFilter.new \
37
+ resource, missing_dependent_filters
38
+ end
39
+
35
40
  each_filter do |filter, operator, value|
36
41
  @scope = filter_scope(filter, operator, value)
37
42
  end
@@ -20,17 +20,32 @@ module Graphiti
20
20
  end
21
21
 
22
22
  def missing_required_filters
23
- required_attributes - filter_param.keys
23
+ required_filters - filter_param.keys
24
24
  end
25
25
 
26
- def required_attributes
27
- resource.attributes.map do |k, v|
28
- k if v[:filterable] == :required
26
+ def required_filters
27
+ resource.filters.map do |k, v|
28
+ k if v[:required]
29
29
  end.compact
30
30
  end
31
31
 
32
- def required_filters_provided?
33
- missing_required_filters.empty?
32
+ def missing_dependent_filters
33
+ [].tap do |arr|
34
+ filter_param.each_pair do |key, value|
35
+ if df = dependent_filters[key]
36
+ missing = df[:dependencies] - filter_param.keys
37
+ unless missing.length.zero?
38
+ arr << { filter: df, missing: missing }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def dependent_filters
46
+ resource.filters.select do |k, v|
47
+ v[:dependencies].present?
48
+ end
34
49
  end
35
50
  end
36
51
  end
@@ -23,14 +23,14 @@ module Graphiti
23
23
  # We should use the default unless the user has customized.
24
24
  # @see Resource.paginate
25
25
  class Scoping::Paginate < Scoping::Base
26
- MAX_PAGE_SIZE = 1_000
26
+ DEFAULT_PAGE_SIZE = 20
27
27
 
28
28
  # Apply the pagination logic. Raise error if over the max page size.
29
29
  # @return the scope object we are chaining/modifying
30
30
  def apply
31
- if size > MAX_PAGE_SIZE
31
+ if size > resource.max_page_size
32
32
  raise Graphiti::Errors::UnsupportedPageSize
33
- .new(size, MAX_PAGE_SIZE)
33
+ .new(size, resource.max_page_size)
34
34
  elsif requested? && @opts[:sideload_parent_length].to_i > 1
35
35
  raise Graphiti::Errors::UnsupportedPagination
36
36
  else
@@ -81,7 +81,7 @@ module Graphiti
81
81
  end
82
82
 
83
83
  def size
84
- (page_param[:size] || resource.default_page_size).to_i
84
+ (page_param[:size] || resource.default_page_size || DEFAULT_PAGE_SIZE).to_i
85
85
  end
86
86
  end
87
87
  end
@@ -22,10 +22,16 @@ module Graphiti
22
22
  # @return the scope we are chaining/modifying
23
23
  def apply_standard_scope
24
24
  each_sort do |attribute, direction|
25
- if sort = resource.sorts[attribute]
26
- @scope = sort.call(@scope, direction)
25
+ sort = resource.sorts[attribute]
26
+ if sort[:only] && sort[:only] != direction
27
+ raise Errors::UnsupportedSort.new resource,
28
+ attribute, sort[:only], direction
27
29
  else
28
- @scope = resource.adapter.order(@scope, attribute, direction)
30
+ if sort[:proc]
31
+ @scope = sort[:proc].call(@scope, direction)
32
+ else
33
+ @scope = resource.adapter.order(@scope, attribute, direction)
34
+ end
29
35
  end
30
36
  end
31
37
  @scope
@@ -54,11 +60,26 @@ module Graphiti
54
60
  def sort_param
55
61
  @sort_param ||= begin
56
62
  if query_hash[:sort].blank?
57
- resource.default_sort
63
+ resource.default_sort || []
58
64
  else
59
- query_hash[:sort]
65
+ normalize(query_hash[:sort])
60
66
  end
61
67
  end
62
68
  end
69
+
70
+ def normalize(sort)
71
+ return sort if sort.is_a?(Array)
72
+ sorts = sort.split(',')
73
+ sorts.map do |s|
74
+ sort_hash(s)
75
+ end
76
+ end
77
+
78
+ def sort_hash(attr)
79
+ value = attr[0] == '-' ? :desc : :asc
80
+ key = attr.sub('-', '').to_sym
81
+
82
+ { key => value }
83
+ end
63
84
  end
64
85
  end