graphiti-rb 1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +20 -0
  6. data/.yardopts +2 -0
  7. data/Appraisals +11 -0
  8. data/CODE_OF_CONDUCT.md +49 -0
  9. data/Gemfile +12 -0
  10. data/Guardfile +32 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +75 -0
  13. data/Rakefile +15 -0
  14. data/bin/appraisal +17 -0
  15. data/bin/console +14 -0
  16. data/bin/rspec +17 -0
  17. data/bin/setup +8 -0
  18. data/gemfiles/rails_4.gemfile +17 -0
  19. data/gemfiles/rails_5.gemfile +17 -0
  20. data/graphiti.gemspec +34 -0
  21. data/lib/generators/jsonapi/resource_generator.rb +169 -0
  22. data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
  23. data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
  24. data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
  25. data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
  26. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
  27. data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
  28. data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
  29. data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
  30. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
  31. data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
  32. data/lib/graphiti-rb.rb +1 -0
  33. data/lib/graphiti.rb +121 -0
  34. data/lib/graphiti/adapters/abstract.rb +516 -0
  35. data/lib/graphiti/adapters/active_record.rb +6 -0
  36. data/lib/graphiti/adapters/active_record/base.rb +249 -0
  37. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
  38. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
  39. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
  40. data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
  41. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
  42. data/lib/graphiti/adapters/null.rb +236 -0
  43. data/lib/graphiti/base.rb +70 -0
  44. data/lib/graphiti/configuration.rb +21 -0
  45. data/lib/graphiti/context.rb +16 -0
  46. data/lib/graphiti/deserializer.rb +208 -0
  47. data/lib/graphiti/errors.rb +309 -0
  48. data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
  49. data/lib/graphiti/extensions/extra_attribute.rb +70 -0
  50. data/lib/graphiti/extensions/temp_id.rb +26 -0
  51. data/lib/graphiti/filter_operators.rb +25 -0
  52. data/lib/graphiti/hash_renderer.rb +57 -0
  53. data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
  54. data/lib/graphiti/query.rb +251 -0
  55. data/lib/graphiti/rails.rb +28 -0
  56. data/lib/graphiti/railtie.rb +74 -0
  57. data/lib/graphiti/renderer.rb +60 -0
  58. data/lib/graphiti/resource.rb +110 -0
  59. data/lib/graphiti/resource/configuration.rb +239 -0
  60. data/lib/graphiti/resource/dsl.rb +138 -0
  61. data/lib/graphiti/resource/interface.rb +32 -0
  62. data/lib/graphiti/resource/polymorphism.rb +68 -0
  63. data/lib/graphiti/resource/sideloading.rb +102 -0
  64. data/lib/graphiti/resource_proxy.rb +127 -0
  65. data/lib/graphiti/responders.rb +19 -0
  66. data/lib/graphiti/runner.rb +25 -0
  67. data/lib/graphiti/scope.rb +98 -0
  68. data/lib/graphiti/scoping/base.rb +99 -0
  69. data/lib/graphiti/scoping/default_filter.rb +58 -0
  70. data/lib/graphiti/scoping/extra_attributes.rb +29 -0
  71. data/lib/graphiti/scoping/filter.rb +93 -0
  72. data/lib/graphiti/scoping/filterable.rb +36 -0
  73. data/lib/graphiti/scoping/paginate.rb +87 -0
  74. data/lib/graphiti/scoping/sort.rb +64 -0
  75. data/lib/graphiti/sideload.rb +281 -0
  76. data/lib/graphiti/sideload/belongs_to.rb +34 -0
  77. data/lib/graphiti/sideload/has_many.rb +16 -0
  78. data/lib/graphiti/sideload/has_one.rb +9 -0
  79. data/lib/graphiti/sideload/many_to_many.rb +24 -0
  80. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
  81. data/lib/graphiti/stats/dsl.rb +89 -0
  82. data/lib/graphiti/stats/payload.rb +49 -0
  83. data/lib/graphiti/types.rb +172 -0
  84. data/lib/graphiti/util/attribute_check.rb +88 -0
  85. data/lib/graphiti/util/field_params.rb +16 -0
  86. data/lib/graphiti/util/hash.rb +51 -0
  87. data/lib/graphiti/util/hooks.rb +33 -0
  88. data/lib/graphiti/util/include_params.rb +39 -0
  89. data/lib/graphiti/util/persistence.rb +219 -0
  90. data/lib/graphiti/util/relationship_payload.rb +64 -0
  91. data/lib/graphiti/util/serializer_attributes.rb +97 -0
  92. data/lib/graphiti/util/sideload.rb +33 -0
  93. data/lib/graphiti/util/validation_response.rb +78 -0
  94. data/lib/graphiti/version.rb +3 -0
  95. metadata +317 -0
@@ -0,0 +1,87 @@
1
+ module Graphiti
2
+ # Apply pagination logic to the scope
3
+ #
4
+ # If the user requests a page size greater than +MAX_PAGE_SIZE+,
5
+ # a +Graphiti::Errors::UnsupportedPageSize+ error will be raised.
6
+ #
7
+ # Notably, this will not fire when the `default: false` option is passed.
8
+ # This is the case for sideloads - if the user requests "give me the post
9
+ # and its comments", we shouldn't implicitly limit those comments to 20.
10
+ # *BUT* if the user requests, "give me the post and 3 of its comments", we
11
+ # *should* honor that pagination.
12
+ #
13
+ # This can be confusing because there are also 'default' and 'customized'
14
+ # pagination procs. The default comes 'for free'. Customized pagination
15
+ # looks like
16
+ #
17
+ # class PostResource < ApplicationResource
18
+ # paginate do |scope, current_page, per_page|
19
+ # # ... the custom logic ...
20
+ # end
21
+ # end
22
+ #
23
+ # We should use the default unless the user has customized.
24
+ # @see Resource.paginate
25
+ class Scoping::Paginate < Scoping::Base
26
+ MAX_PAGE_SIZE = 1_000
27
+
28
+ # Apply the pagination logic. Raise error if over the max page size.
29
+ # @return the scope object we are chaining/modifying
30
+ def apply
31
+ if size > MAX_PAGE_SIZE
32
+ raise Graphiti::Errors::UnsupportedPageSize
33
+ .new(size, MAX_PAGE_SIZE)
34
+ elsif requested? && @opts[:sideload_parent_length].to_i > 1
35
+ raise Graphiti::Errors::UnsupportedPagination
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ # We want to apply this logic unless we've explicitly received the
42
+ # +default: false+ option. In that case, only apply if pagination
43
+ # was explicitly specified in the request.
44
+ #
45
+ # @return [Boolean] should we apply this logic?
46
+ def apply?
47
+ if @opts[:default_paginate] == false
48
+ requested?
49
+ else
50
+ true
51
+ end
52
+ end
53
+
54
+ # @return [Proc, Nil] the custom pagination proc
55
+ def custom_scope
56
+ resource.pagination
57
+ end
58
+
59
+ # Apply default pagination proc via the Resource adapter
60
+ def apply_standard_scope
61
+ resource.adapter.paginate(@scope, number, size)
62
+ end
63
+
64
+ # Apply the custom pagination proc
65
+ def apply_custom_scope
66
+ custom_scope.call(@scope, number, size, resource.context)
67
+ end
68
+
69
+ private
70
+
71
+ def requested?
72
+ not [page_param[:size], page_param[:number]].all?(&:nil?)
73
+ end
74
+
75
+ def page_param
76
+ @page_param ||= (query_hash[:page] || {})
77
+ end
78
+
79
+ def number
80
+ (page_param[:number] || 1).to_i
81
+ end
82
+
83
+ def size
84
+ (page_param[:size] || resource.default_page_size).to_i
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,64 @@
1
+ module Graphiti
2
+ # Apply sorting logic to the scope.
3
+ #
4
+ # By default, sorting comes 'for free'. To specify a custom sorting proc:
5
+ #
6
+ # class PostResource < ApplicationResource
7
+ # sort do |scope, att, dir|
8
+ # int = dir == :desc ? -1 : 1
9
+ # scope.sort_by { |x| x[att] * int }
10
+ # end
11
+ # end
12
+ #
13
+ # The sorting proc will be called once for each sort att/dir requested.
14
+ # @see Resource.sort
15
+ class Scoping::Sort < Scoping::Base
16
+ # @return [Proc, Nil] The custom proc specified by Resource DSL
17
+ def custom_scope
18
+ resource.sort_all
19
+ end
20
+
21
+ # Apply default scope logic via Resource adapter
22
+ # @return the scope we are chaining/modifying
23
+ def apply_standard_scope
24
+ each_sort do |attribute, direction|
25
+ if sort = resource.sorts[attribute]
26
+ @scope = sort.call(@scope, direction)
27
+ else
28
+ @scope = resource.adapter.order(@scope, attribute, direction)
29
+ end
30
+ end
31
+ @scope
32
+ end
33
+
34
+ # Apply custom scoping proc configured via Resource DSL
35
+ # @return the scope we are chaining/modifying
36
+ def apply_custom_scope
37
+ each_sort do |attribute, direction|
38
+ @scope = custom_scope
39
+ .call(@scope, attribute, direction, resource.context)
40
+ end
41
+ @scope
42
+ end
43
+
44
+ private
45
+
46
+ def each_sort
47
+ sort_param.each do |sort_hash|
48
+ attribute = sort_hash.keys.first
49
+ direction = sort_hash.values.first
50
+ yield attribute, direction
51
+ end
52
+ end
53
+
54
+ def sort_param
55
+ @sort_param ||= begin
56
+ if query_hash[:sort].blank?
57
+ resource.default_sort
58
+ else
59
+ query_hash[:sort]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,281 @@
1
+ module Graphiti
2
+ class Sideload
3
+ HOOK_ACTIONS = [:save, :create, :update, :destroy, :disassociate]
4
+ TYPES = [:has_many, :belongs_to, :has_one, :many_to_many]
5
+
6
+ attr_reader :name,
7
+ :resource_class,
8
+ :parent_resource_class,
9
+ :foreign_key,
10
+ :primary_key,
11
+ :parent,
12
+ :group_name
13
+
14
+ class_attribute :scope_proc,
15
+ :assign_proc,
16
+ :assign_each_proc,
17
+ :params_proc,
18
+ :pre_load_proc
19
+
20
+ def initialize(name, opts)
21
+ @name = name
22
+ @parent_resource_class = opts[:parent_resource]
23
+ @resource_class = opts[:resource]
24
+ @primary_key = opts[:primary_key]
25
+ @foreign_key = opts[:foreign_key]
26
+ @type = opts[:type]
27
+ @base_scope = opts[:base_scope]
28
+ @readable = opts[:readable]
29
+ @writable = opts[:writable]
30
+ @as = opts[:as]
31
+
32
+ # polymorphic-specific
33
+ @group_name = opts[:group_name]
34
+ @polymorphic_child = opts[:polymorphic_child]
35
+ @parent = opts[:parent]
36
+ if polymorphic_child?
37
+ parent.resource.polymorphic << resource_class
38
+ end
39
+ end
40
+
41
+ def self.scope(&blk)
42
+ self.scope_proc = blk
43
+ end
44
+
45
+ def self.assign(&blk)
46
+ self.assign_proc = blk
47
+ end
48
+
49
+ def self.assign_each(&blk)
50
+ self.assign_each_proc = blk
51
+ end
52
+
53
+ def self.params(&blk)
54
+ self.params_proc = blk
55
+ end
56
+
57
+ def self.pre_load(&blk)
58
+ self.pre_load_proc = blk
59
+ end
60
+
61
+ def readable?
62
+ !!@readable
63
+ end
64
+
65
+ def writable?
66
+ !!@writable
67
+ end
68
+
69
+ def polymorphic_parent?
70
+ resource.polymorphic?
71
+ end
72
+
73
+ def polymorphic_child?
74
+ !!@polymorphic_child
75
+ end
76
+
77
+ def primary_key
78
+ @primary_key ||= :id
79
+ end
80
+
81
+ def foreign_key
82
+ @foreign_key ||= infer_foreign_key
83
+ end
84
+
85
+ def association_name
86
+ @as || name
87
+ end
88
+
89
+ def resource_class
90
+ @resource_class ||= infer_resource_class
91
+ end
92
+
93
+ def scope(parents)
94
+ raise "No #scope defined for sideload with name '#{name}'. Make sure to define this in your adapter, or pass a block that defines the scope."
95
+ end
96
+
97
+ def assign_each(parent, children)
98
+ raise 'Override #assign_each in subclass'
99
+ end
100
+
101
+ def type
102
+ @type || raise("Override #type in subclass. Should be one of #{TYPES.inspect}")
103
+ end
104
+
105
+ def load_params(parents, query)
106
+ raise 'Override #load_params in subclass'
107
+ end
108
+
109
+ def base_scope
110
+ if @base_scope
111
+ @base_scope.respond_to?(:call) ? @base_scope.call : @base_scope
112
+ else
113
+ resource.base_scope
114
+ end
115
+ end
116
+
117
+ def load(parents, query)
118
+ params = load_params(parents, query)
119
+ params_proc.call(params, parents, query) if params_proc
120
+ opts = load_options(parents, query)
121
+ proxy = resource.class._all(params, opts, base_scope)
122
+ pre_load_proc.call(proxy) if pre_load_proc
123
+ proxy.to_a
124
+ end
125
+
126
+ # Override in subclass
127
+ def infer_foreign_key
128
+ model = parent_resource_class.model
129
+ namespace = namespace_for(model)
130
+ model_name = model.name.gsub("#{namespace}::", '')
131
+ :"#{model_name.underscore}_id"
132
+ end
133
+
134
+ def resource
135
+ @resource ||= resource_class.new
136
+ end
137
+
138
+ def parent_resource
139
+ @parent_resource ||= parent_resource_class.new
140
+ end
141
+
142
+ def assign(parents, children)
143
+ associated = []
144
+ parents.each do |parent|
145
+ relevant_children = fire_assign_each(parent, children)
146
+ if relevant_children.is_a?(Array)
147
+ associated |= relevant_children
148
+ associate_all(parent, relevant_children)
149
+ else
150
+ associated << relevant_children
151
+ associate(parent, relevant_children)
152
+ end
153
+ end
154
+ (children - associated).each do |unassigned|
155
+ children.delete(unassigned)
156
+ end
157
+ end
158
+
159
+ def resolve(parents, query)
160
+ # legacy / custom / many-to-many
161
+ if self.class.scope_proc || type == :many_to_many
162
+ sideload_scope = fire_scope(parents)
163
+ sideload_scope = Scope.new sideload_scope,
164
+ resource,
165
+ query,
166
+ sideload_parent_length: parents.length,
167
+ default_paginate: false
168
+ sideload_scope.resolve do |sideload_results|
169
+ fire_assign(parents, sideload_results)
170
+ end
171
+ else
172
+ load(parents, query)
173
+ end
174
+ end
175
+
176
+ def self.after_save(only: [], except: [], &blk)
177
+ actions = HOOK_ACTIONS - except
178
+ actions = only & actions
179
+ actions = [:save] if only.empty? && except.empty?
180
+ actions.each do |a|
181
+ hooks[:"after_#{a}"] << blk
182
+ end
183
+ end
184
+
185
+ def self.hooks
186
+ @hooks ||= {}.tap do |h|
187
+ HOOK_ACTIONS.each do |a|
188
+ h[:"after_#{a}"] = []
189
+ h[:"before_#{a}"] = []
190
+ end
191
+ end
192
+ end
193
+
194
+ def fire_hooks!(parent, objects, method)
195
+ return unless self.class.hooks
196
+
197
+ all = self.class.hooks[:"after_#{method}"] + self.class.hooks[:after_save]
198
+ all.compact.each do |hook|
199
+ resource.instance_exec(parent, objects, &hook)
200
+ end
201
+ end
202
+
203
+ def associate_all(parent, children)
204
+ parent_resource.associate_all(parent, children, association_name, type)
205
+ end
206
+
207
+ def associate(parent, child)
208
+ parent_resource.associate(parent, child, association_name, type)
209
+ end
210
+
211
+ def disassociate(parent, child)
212
+ parent_resource.disassociate(parent, child, association_name, type)
213
+ end
214
+
215
+ def ids_for_parents(parents)
216
+ parent_ids = parents.map(&primary_key)
217
+ parent_ids.compact!
218
+ parent_ids.uniq!
219
+ parent_ids
220
+ end
221
+
222
+ private
223
+
224
+ def load_options(parents, query)
225
+ {}.tap do |opts|
226
+ opts[:default_paginate] = false
227
+ opts[:sideload_parent_length] = parents.length
228
+ opts[:after_resolve] = ->(results) {
229
+ fire_assign(parents, results)
230
+ }
231
+ end
232
+ end
233
+
234
+ def fire_assign_each(parent, children)
235
+ if self.class.assign_each_proc
236
+ instance_exec(parent, children, &self.class.assign_each_proc)
237
+ else
238
+ assign_each(parent, children)
239
+ end
240
+ end
241
+
242
+ def fire_assign(parents, children)
243
+ if self.class.assign_proc
244
+ instance_exec(parents, children, &self.class.assign_proc)
245
+ else
246
+ assign(parents, children)
247
+ end
248
+ end
249
+
250
+ def fire_scope(parents)
251
+ parent_ids = ids_for_parents(parents)
252
+ if self.class.scope_proc
253
+ instance_exec(parent_ids, parents, &self.class.scope_proc)
254
+ else
255
+ method = method(:scope)
256
+ if [2,-2].include?(method.arity)
257
+ scope(parent_ids, parents)
258
+ else
259
+ scope(parent_ids)
260
+ end
261
+ end
262
+ end
263
+
264
+ def namespace_for(klass)
265
+ namespace = klass.name
266
+ split = namespace.split('::')
267
+ split[0,split.length-1].join('::')
268
+ end
269
+
270
+ def infer_resource_class
271
+ namespace = namespace_for(parent_resource.class)
272
+ inferred_name = "#{name.to_s.singularize.classify}Resource"
273
+ klass = "#{namespace}::#{inferred_name}".safe_constantize
274
+ klass ||= inferred_name.safe_constantize
275
+ unless klass
276
+ raise Errors::ResourceNotFound.new(parent_resource, name)
277
+ end
278
+ klass
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,34 @@
1
+ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
2
+ def type
3
+ :belongs_to
4
+ end
5
+
6
+ def load_params(parents, query)
7
+ query.to_hash.tap do |hash|
8
+ hash[:filter] ||= {}
9
+ hash[:filter][primary_key] = ids_for_parents(parents)
10
+ end
11
+ end
12
+
13
+ def assign_each(parent, children)
14
+ children.find { |c| c.send(primary_key) == parent.send(foreign_key) }
15
+ end
16
+
17
+ def ids_for_parents(parents)
18
+ parent_ids = parents.map(&foreign_key)
19
+ parent_ids.compact!
20
+ parent_ids.uniq!
21
+ parent_ids
22
+ end
23
+
24
+ def infer_foreign_key
25
+ if polymorphic_child?
26
+ parent.foreign_key
27
+ else
28
+ model = resource.model
29
+ namespace = namespace_for(model)
30
+ model_name = model.name.gsub("#{namespace}::", '')
31
+ :"#{model_name.underscore}_id"
32
+ end
33
+ end
34
+ end