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