jsonapi-resources 0.9.3 → 0.10.5

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 (44) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +297 -0
  8. data/lib/jsonapi/active_relation_resource.rb +879 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -105
  10. data/lib/jsonapi/basic_resource.rb +1162 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +127 -0
  12. data/lib/jsonapi/compiled_json.rb +11 -1
  13. data/lib/jsonapi/configuration.rb +63 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +63 -40
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -65
  19. data/lib/jsonapi/link_builder.rb +74 -80
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +74 -16
  22. data/lib/jsonapi/path.rb +43 -0
  23. data/lib/jsonapi/path_segment.rb +76 -0
  24. data/lib/jsonapi/processor.rb +237 -110
  25. data/lib/jsonapi/relationship.rb +144 -15
  26. data/lib/jsonapi/request_parser.rb +412 -357
  27. data/lib/jsonapi/resource.rb +3 -1263
  28. data/lib/jsonapi/resource_controller_metal.rb +5 -2
  29. data/lib/jsonapi/resource_fragment.rb +47 -0
  30. data/lib/jsonapi/resource_id_tree.rb +112 -0
  31. data/lib/jsonapi/resource_identity.rb +42 -0
  32. data/lib/jsonapi/resource_serializer.rb +143 -285
  33. data/lib/jsonapi/resource_set.rb +176 -0
  34. data/lib/jsonapi/resources/railtie.rb +9 -0
  35. data/lib/jsonapi/resources/version.rb +1 -1
  36. data/lib/jsonapi/response_document.rb +105 -83
  37. data/lib/jsonapi/routing_ext.rb +48 -26
  38. data/lib/jsonapi-resources.rb +20 -4
  39. data/lib/tasks/check_upgrade.rake +52 -0
  40. metadata +47 -17
  41. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  42. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  43. data/lib/jsonapi/operation_results.rb +0 -35
  44. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -1,1265 +1,5 @@
1
- require 'jsonapi/callbacks'
2
- require 'jsonapi/relationship_builder'
3
-
4
1
  module JSONAPI
5
- class Resource
6
- include Callbacks
7
-
8
- attr_reader :context
9
-
10
- define_jsonapi_resources_callbacks :create,
11
- :update,
12
- :remove,
13
- :save,
14
- :create_to_many_link,
15
- :replace_to_many_links,
16
- :create_to_one_link,
17
- :replace_to_one_link,
18
- :replace_polymorphic_to_one_link,
19
- :remove_to_many_link,
20
- :remove_to_one_link,
21
- :replace_fields
22
-
23
- def initialize(model, context)
24
- @model = model
25
- @context = context
26
- @reload_needed = false
27
- @changing = false
28
- @save_needed = false
29
- end
30
-
31
- def _model
32
- @model
33
- end
34
-
35
- def id
36
- _model.public_send(self.class._primary_key)
37
- end
38
-
39
- def cache_id
40
- [id, _model.public_send(self.class._cache_field)]
41
- end
42
-
43
- def is_new?
44
- id.nil?
45
- end
46
-
47
- def change(callback)
48
- completed = false
49
-
50
- if @changing
51
- run_callbacks callback do
52
- completed = (yield == :completed)
53
- end
54
- else
55
- run_callbacks is_new? ? :create : :update do
56
- @changing = true
57
- run_callbacks callback do
58
- completed = (yield == :completed)
59
- end
60
-
61
- completed = (save == :completed) if @save_needed || is_new?
62
- end
63
- end
64
-
65
- return completed ? :completed : :accepted
66
- end
67
-
68
- def remove
69
- run_callbacks :remove do
70
- _remove
71
- end
72
- end
73
-
74
- def create_to_many_links(relationship_type, relationship_key_values, options = {})
75
- change :create_to_many_link do
76
- _create_to_many_links(relationship_type, relationship_key_values, options)
77
- end
78
- end
79
-
80
- def replace_to_many_links(relationship_type, relationship_key_values, options = {})
81
- change :replace_to_many_links do
82
- _replace_to_many_links(relationship_type, relationship_key_values, options)
83
- end
84
- end
85
-
86
- def replace_to_one_link(relationship_type, relationship_key_value, options = {})
87
- change :replace_to_one_link do
88
- _replace_to_one_link(relationship_type, relationship_key_value, options)
89
- end
90
- end
91
-
92
- def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {})
93
- change :replace_polymorphic_to_one_link do
94
- _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options)
95
- end
96
- end
97
-
98
- def remove_to_many_link(relationship_type, key, options = {})
99
- change :remove_to_many_link do
100
- _remove_to_many_link(relationship_type, key, options)
101
- end
102
- end
103
-
104
- def remove_to_one_link(relationship_type, options = {})
105
- change :remove_to_one_link do
106
- _remove_to_one_link(relationship_type, options)
107
- end
108
- end
109
-
110
- def replace_fields(field_data)
111
- change :replace_fields do
112
- _replace_fields(field_data)
113
- end
114
- end
115
-
116
- # Override this on a resource instance to override the fetchable keys
117
- def fetchable_fields
118
- self.class.fields
119
- end
120
-
121
- # Override this on a resource to customize how the associated records
122
- # are fetched for a model. Particularly helpful for authorization.
123
- def records_for(relation_name)
124
- _model.public_send relation_name
125
- end
126
-
127
- def model_error_messages
128
- _model.errors.messages
129
- end
130
-
131
- # Add metadata to validation error objects.
132
- #
133
- # Suppose `model_error_messages` returned the following error messages
134
- # hash:
135
- #
136
- # {password: ["too_short", "format"]}
137
- #
138
- # Then to add data to the validation error `validation_error_metadata`
139
- # could return:
140
- #
141
- # {
142
- # password: {
143
- # "too_short": {"minimum_length" => 6},
144
- # "format": {"requirement" => "must contain letters and numbers"}
145
- # }
146
- # }
147
- #
148
- # The specified metadata is then be merged into the validation error
149
- # object.
150
- def validation_error_metadata
151
- {}
152
- end
153
-
154
- # Override this to return resource level meta data
155
- # must return a hash, and if the hash is empty the meta section will not be serialized with the resource
156
- # meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
157
- # serializer's format_key and format_value methods if desired
158
- # the _options hash will contain the serializer and the serialization_options
159
- def meta(_options)
160
- {}
161
- end
162
-
163
- # Override this to return custom links
164
- # must return a hash, which will be merged with the default { self: 'self-url' } links hash
165
- # links keys will be not be formatted with the key formatter for the serializer by default.
166
- # They can however use the serializer's format_key and format_value methods if desired
167
- # the _options hash will contain the serializer and the serialization_options
168
- def custom_links(_options)
169
- {}
170
- end
171
-
172
- def preloaded_fragments
173
- # A hash of hashes
174
- @preloaded_fragments ||= Hash.new
175
- end
176
-
177
- private
178
-
179
- def save
180
- run_callbacks :save do
181
- _save
182
- end
183
- end
184
-
185
- # Override this on a resource to return a different result code. Any
186
- # value other than :completed will result in operations returning
187
- # `:accepted`
188
- #
189
- # For example to return `:accepted` if your model does not immediately
190
- # save resources to the database you could override `_save` as follows:
191
- #
192
- # ```
193
- # def _save
194
- # super
195
- # return :accepted
196
- # end
197
- # ```
198
- def _save(validation_context = nil)
199
- unless @model.valid?(validation_context)
200
- fail JSONAPI::Exceptions::ValidationErrors.new(self)
201
- end
202
-
203
- if defined? @model.save
204
- saved = @model.save(validate: false)
205
-
206
- unless saved
207
- if @model.errors.present?
208
- fail JSONAPI::Exceptions::ValidationErrors.new(self)
209
- else
210
- fail JSONAPI::Exceptions::SaveFailed.new
211
- end
212
- end
213
- else
214
- saved = true
215
- end
216
- @model.reload if @reload_needed
217
- @reload_needed = false
218
-
219
- @save_needed = !saved
220
-
221
- :completed
222
- end
223
-
224
- def _remove
225
- unless @model.destroy
226
- fail JSONAPI::Exceptions::ValidationErrors.new(self)
227
- end
228
- :completed
229
-
230
- rescue ActiveRecord::DeleteRestrictionError => e
231
- fail JSONAPI::Exceptions::RecordLocked.new(e.message)
232
- end
233
-
234
- def reflect_relationship?(relationship, options)
235
- return false if !relationship.reflect ||
236
- (!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source])
237
-
238
- inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship]
239
- if inverse_relationship.nil?
240
- warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled."
241
- return false
242
- end
243
- true
244
- end
245
-
246
- def _create_to_many_links(relationship_type, relationship_key_values, options)
247
- relationship = self.class._relationships[relationship_type]
248
-
249
- # check if relationship_key_values are already members of this relationship
250
- relation_name = relationship.relation_name(context: @context)
251
- existing_relations = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_values)
252
- if existing_relations.count > 0
253
- # todo: obscure id so not to leak info
254
- fail JSONAPI::Exceptions::HasManyRelationExists.new(existing_relations.first.id)
255
- end
256
-
257
- if options[:reflected_source]
258
- @model.public_send(relation_name) << options[:reflected_source]._model
259
- return :completed
260
- end
261
-
262
- # load requested related resources
263
- # make sure they all exist (also based on context) and add them to relationship
264
-
265
- related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context)
266
-
267
- if related_resources.count != relationship_key_values.count
268
- # todo: obscure id so not to leak info
269
- fail JSONAPI::Exceptions::RecordNotFound.new('unspecified')
270
- end
271
-
272
- reflect = reflect_relationship?(relationship, options)
273
-
274
- related_resources.each do |related_resource|
275
- if reflect
276
- if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
277
- related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self)
278
- else
279
- related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self)
280
- end
281
- @reload_needed = true
282
- else
283
- @model.public_send(relation_name) << related_resource._model
284
- end
285
- end
286
-
287
- :completed
288
- end
289
-
290
- def _replace_to_many_links(relationship_type, relationship_key_values, options)
291
- relationship = self.class._relationships[relationship_type]
292
-
293
- reflect = reflect_relationship?(relationship, options)
294
-
295
- if reflect
296
- existing = send("#{relationship.foreign_key}")
297
- to_delete = existing - (relationship_key_values & existing)
298
- to_delete.each do |key|
299
- _remove_to_many_link(relationship_type, key, reflected_source: self)
300
- end
301
-
302
- to_add = relationship_key_values - (relationship_key_values & existing)
303
- _create_to_many_links(relationship_type, to_add, {})
304
-
305
- @reload_needed = true
306
- else
307
- send("#{relationship.foreign_key}=", relationship_key_values)
308
- @save_needed = true
309
- end
310
-
311
- :completed
312
- end
313
-
314
- def _replace_to_one_link(relationship_type, relationship_key_value, options)
315
- relationship = self.class._relationships[relationship_type]
316
-
317
- send("#{relationship.foreign_key}=", relationship_key_value)
318
- @save_needed = true
319
-
320
- :completed
321
- end
322
-
323
- def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, options)
324
- relationship = self.class._relationships[relationship_type.to_sym]
325
-
326
- _model.public_send("#{relationship.foreign_key}=", key_value)
327
- _model.public_send("#{relationship.polymorphic_type}=", _model_class_name(key_type))
328
-
329
- @save_needed = true
330
-
331
- :completed
332
- end
333
-
334
- def _remove_to_many_link(relationship_type, key, options)
335
- relationship = self.class._relationships[relationship_type]
336
-
337
- reflect = reflect_relationship?(relationship, options)
338
-
339
- if reflect
340
-
341
- related_resource = relationship.resource_klass.find_by_key(key, context: @context)
342
-
343
- if related_resource.nil?
344
- fail JSONAPI::Exceptions::RecordNotFound.new(key)
345
- else
346
- if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
347
- related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self)
348
- else
349
- related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self)
350
- end
351
- end
352
-
353
- @reload_needed = true
354
- else
355
- @model.public_send(relationship.relation_name(context: @context)).destroy(key)
356
- end
357
-
358
- :completed
359
-
360
- rescue ActiveRecord::DeleteRestrictionError => e
361
- fail JSONAPI::Exceptions::RecordLocked.new(e.message)
362
- rescue ActiveRecord::RecordNotFound
363
- fail JSONAPI::Exceptions::RecordNotFound.new(key)
364
- end
365
-
366
- def _remove_to_one_link(relationship_type, options)
367
- relationship = self.class._relationships[relationship_type]
368
-
369
- send("#{relationship.foreign_key}=", nil)
370
- @save_needed = true
371
-
372
- :completed
373
- end
374
-
375
- def _replace_fields(field_data)
376
- field_data[:attributes].each do |attribute, value|
377
- begin
378
- send "#{attribute}=", value
379
- @save_needed = true
380
- rescue ArgumentError
381
- # :nocov: Will be thrown if an enum value isn't allowed for an enum. Currently not tested as enums are a rails 4.1 and higher feature
382
- raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value)
383
- # :nocov:
384
- end
385
- end
386
-
387
- field_data[:to_one].each do |relationship_type, value|
388
- if value.nil?
389
- remove_to_one_link(relationship_type)
390
- else
391
- case value
392
- when Hash
393
- replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
394
- else
395
- replace_to_one_link(relationship_type, value)
396
- end
397
- end
398
- end if field_data[:to_one]
399
-
400
- field_data[:to_many].each do |relationship_type, values|
401
- replace_to_many_links(relationship_type, values)
402
- end if field_data[:to_many]
403
-
404
- :completed
405
- end
406
-
407
- def _model_class_name(key_type)
408
- type_class_name = key_type.to_s.classify
409
- resource = self.class.resource_for(type_class_name)
410
- resource ? resource._model_name.to_s : type_class_name
411
- end
412
-
413
- class << self
414
- def inherited(subclass)
415
- subclass.abstract(false)
416
- subclass.immutable(false)
417
- subclass.caching(false)
418
- subclass._attributes = (_attributes || {}).dup
419
-
420
- subclass._model_hints = (_model_hints || {}).dup
421
-
422
- unless _model_name.empty?
423
- subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
424
- end
425
-
426
- subclass.rebuild_relationships(_relationships || {})
427
-
428
- subclass._allowed_filters = (_allowed_filters || Set.new).dup
429
-
430
- type = subclass.name.demodulize.sub(/Resource$/, '').underscore
431
- subclass._type = type.pluralize.to_sym
432
-
433
- unless subclass._attributes[:id]
434
- subclass.attribute :id, format: :id
435
- end
436
-
437
- check_reserved_resource_name(subclass._type, subclass.name)
438
- end
439
-
440
- def rebuild_relationships(relationships)
441
- original_relationships = relationships.deep_dup
442
-
443
- @_relationships = {}
444
-
445
- if original_relationships.is_a?(Hash)
446
- original_relationships.each_value do |relationship|
447
- options = relationship.options.dup
448
- options[:parent_resource] = self
449
- _add_relationship(relationship.class, relationship.name, options)
450
- end
451
- end
452
- end
453
-
454
- def resource_for(type)
455
- type = type.underscore
456
- type_with_module = type.start_with?(module_path) ? type : module_path + type
457
-
458
- resource_name = _resource_name_from_type(type_with_module)
459
- resource = resource_name.safe_constantize if resource_name
460
- if resource.nil?
461
- fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
462
- end
463
- resource
464
- end
465
-
466
- def resource_for_model(model)
467
- resource_for(resource_type_for(model))
468
- end
469
-
470
- def _resource_name_from_type(type)
471
- "#{type.to_s.underscore.singularize}_resource".camelize
472
- end
473
-
474
- def resource_type_for(model)
475
- model_name = model.class.to_s.underscore
476
- if _model_hints[model_name]
477
- _model_hints[model_name]
478
- else
479
- model_name.rpartition('/').last
480
- end
481
- end
482
-
483
- attr_accessor :_attributes, :_relationships, :_type, :_model_hints
484
- attr_writer :_allowed_filters, :_paginator
485
-
486
- def create(context)
487
- new(create_model, context)
488
- end
489
-
490
- def create_model
491
- _model_class.new
492
- end
493
-
494
- def routing_options(options)
495
- @_routing_resource_options = options
496
- end
497
-
498
- def routing_resource_options
499
- @_routing_resource_options ||= {}
500
- end
501
-
502
- # Methods used in defining a resource class
503
- def attributes(*attrs)
504
- options = attrs.extract_options!.dup
505
- attrs.each do |attr|
506
- attribute(attr, options)
507
- end
508
- end
509
-
510
- def attribute(attribute_name, options = {})
511
- attr = attribute_name.to_sym
512
-
513
- check_reserved_attribute_name(attr)
514
-
515
- if (attr == :id) && (options[:format].nil?)
516
- ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
517
- end
518
-
519
- check_duplicate_attribute_name(attr) if options[:format].nil?
520
-
521
- @_attributes ||= {}
522
- @_attributes[attr] = options
523
- define_method attr do
524
- @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
525
- end unless method_defined?(attr)
526
-
527
- define_method "#{attr}=" do |value|
528
- @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
529
- end unless method_defined?("#{attr}=")
530
- end
531
-
532
- def default_attribute_options
533
- { format: :default }
534
- end
535
-
536
- def relationship(*attrs)
537
- options = attrs.extract_options!
538
- klass = case options[:to]
539
- when :one
540
- Relationship::ToOne
541
- when :many
542
- Relationship::ToMany
543
- else
544
- #:nocov:#
545
- fail ArgumentError.new('to: must be either :one or :many')
546
- #:nocov:#
547
- end
548
- _add_relationship(klass, *attrs, options.except(:to))
549
- end
550
-
551
- def has_one(*attrs)
552
- _add_relationship(Relationship::ToOne, *attrs)
553
- end
554
-
555
- def belongs_to(*attrs)
556
- ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
557
- " using the `belongs_to` class method. We think `has_one`" \
558
- " is more appropriate. If you know what you're doing," \
559
- " and don't want to see this warning again, override the" \
560
- " `belongs_to` class method on your resource."
561
- _add_relationship(Relationship::ToOne, *attrs)
562
- end
563
-
564
- def has_many(*attrs)
565
- _add_relationship(Relationship::ToMany, *attrs)
566
- end
567
-
568
- def model_name(model, options = {})
569
- @_model_name = model.to_sym
570
-
571
- model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
572
-
573
- rebuild_relationships(_relationships)
574
- end
575
-
576
- def model_hint(model: _model_name, resource: _type)
577
- resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
578
-
579
- _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
580
- end
581
-
582
- def filters(*attrs)
583
- @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
584
- end
585
-
586
- def filter(attr, *args)
587
- @_allowed_filters[attr.to_sym] = args.extract_options!
588
- end
589
-
590
- def primary_key(key)
591
- @_primary_key = key.to_sym
592
- end
593
-
594
- def cache_field(field)
595
- @_cache_field = field.to_sym
596
- end
597
-
598
- # Override in your resource to filter the updatable keys
599
- def updatable_fields(_context = nil)
600
- _updatable_relationships | _attributes.keys - [:id]
601
- end
602
-
603
- # Override in your resource to filter the creatable keys
604
- def creatable_fields(_context = nil)
605
- _updatable_relationships | _attributes.keys - [:id]
606
- end
607
-
608
- # Override in your resource to filter the sortable keys
609
- def sortable_fields(_context = nil)
610
- _attributes.keys
611
- end
612
-
613
- def fields
614
- _relationships.keys | _attributes.keys
615
- end
616
-
617
- def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
618
- case model_includes
619
- when Array
620
- return model_includes.map do |value|
621
- resolve_relationship_names_to_relations(resource_klass, value, options)
622
- end
623
- when Hash
624
- model_includes.keys.each do |key|
625
- relationship = resource_klass._relationships[key]
626
- value = model_includes[key]
627
- model_includes.delete(key)
628
- model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
629
- end
630
- return model_includes
631
- when Symbol
632
- relationship = resource_klass._relationships[model_includes]
633
- return relationship.relation_name(options)
634
- end
635
- end
636
-
637
- def apply_includes(records, options = {})
638
- include_directives = options[:include_directives]
639
- if include_directives
640
- model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
641
- records = records.includes(model_includes)
642
- end
643
-
644
- records
645
- end
646
-
647
- def apply_pagination(records, paginator, order_options)
648
- records = paginator.apply(records, order_options) if paginator
649
- records
650
- end
651
-
652
- def apply_sort(records, order_options, _context = {})
653
- if order_options.any?
654
- order_options.each_pair do |field, direction|
655
- if field.to_s.include?(".")
656
- *model_names, column_name = field.split(".")
657
-
658
- associations = _lookup_association_chain([records.model.to_s, *model_names])
659
- joins_query = _build_joins([records.model, *associations])
660
-
661
- # _sorting is appended to avoid name clashes with manual joins eg. overridden filters
662
- order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
663
- records = records.joins(joins_query).order(order_by_query)
664
- else
665
- records = records.order(field => direction)
666
- end
667
- end
668
- end
669
-
670
- records
671
- end
672
-
673
- def _lookup_association_chain(model_names)
674
- associations = []
675
- model_names.inject do |prev, current|
676
- association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
677
- assoc.name.to_s.downcase == current.downcase
678
- end
679
- associations << association
680
- association.class_name
681
- end
682
-
683
- associations
684
- end
685
-
686
- def _build_joins(associations)
687
- joins = []
688
-
689
- associations.inject do |prev, current|
690
- joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
691
- current
692
- end
693
- joins.join("\n")
694
- end
695
-
696
- def apply_filter(records, filter, value, options = {})
697
- strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
698
-
699
- if strategy
700
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
701
- send(strategy, records, value, options)
702
- else
703
- strategy.call(records, value, options)
704
- end
705
- else
706
- records.where(filter => value)
707
- end
708
- end
709
-
710
- def apply_filters(records, filters, options = {})
711
- required_includes = []
712
-
713
- if filters
714
- filters.each do |filter, value|
715
- if _relationships.include?(filter)
716
- if _relationships[filter].belongs_to?
717
- records = apply_filter(records, _relationships[filter].foreign_key, value, options)
718
- else
719
- required_includes.push(filter.to_s)
720
- records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
721
- end
722
- else
723
- records = apply_filter(records, filter, value, options)
724
- end
725
- end
726
- end
727
-
728
- if required_includes.any?
729
- records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
730
- end
731
-
732
- records
733
- end
734
-
735
- def filter_records(filters, options, records = records(options))
736
- records = apply_filters(records, filters, options)
737
- apply_includes(records, options)
738
- end
739
-
740
- def sort_records(records, order_options, context = {})
741
- apply_sort(records, order_options, context)
742
- end
743
-
744
- # Assumes ActiveRecord's counting. Override if you need a different counting method
745
- def count_records(records)
746
- records.count(:all)
747
- end
748
-
749
- def find_count(filters, options = {})
750
- count_records(filter_records(filters, options))
751
- end
752
-
753
- def find(filters, options = {})
754
- resources_for(find_records(filters, options), options[:context])
755
- end
756
-
757
- def resources_for(records, context)
758
- records.collect do |model|
759
- resource_class = self.resource_for_model(model)
760
- resource_class.new(model, context)
761
- end
762
- end
763
-
764
- def find_by_keys(keys, options = {})
765
- context = options[:context]
766
- records = records(options)
767
- records = apply_includes(records, options)
768
- models = records.where({_primary_key => keys})
769
- models.collect do |model|
770
- self.resource_for_model(model).new(model, context)
771
- end
772
- end
773
-
774
- def find_serialized_with_caching(filters_or_source, serializer, options = {})
775
- if filters_or_source.is_a?(ActiveRecord::Relation)
776
- records = filters_or_source
777
- elsif _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
778
- records = find_records(filters_or_source, options.except(:include_directives))
779
- else
780
- records = find(filters_or_source, options)
781
- end
782
- cached_resources_for(records, serializer, options)
783
- end
784
-
785
- def find_by_key(key, options = {})
786
- context = options[:context]
787
- records = find_records({_primary_key => key}, options.except(:paginator, :sort_criteria))
788
- model = records.first
789
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
790
- self.resource_for_model(model).new(model, context)
791
- end
792
-
793
- def find_by_key_serialized_with_caching(key, serializer, options = {})
794
- if _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
795
- results = find_serialized_with_caching({_primary_key => key}, serializer, options)
796
- result = results.first
797
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil?
798
- return result
799
- else
800
- resource = find_by_key(key, options)
801
- return cached_resources_for([resource], serializer, options).first
802
- end
803
- end
804
-
805
- # Override this method if you want to customize the relation for
806
- # finder methods (find, find_by_key, find_serialized_with_caching)
807
- def records(_options = {})
808
- _model_class.all
809
- end
810
-
811
- def verify_filters(filters, context = nil)
812
- verified_filters = {}
813
-
814
- return verified_filters if filters.nil?
815
-
816
- filters.each do |filter, raw_value|
817
- verified_filter = verify_filter(filter, raw_value, context)
818
- verified_filters[verified_filter[0]] = verified_filter[1]
819
- end
820
- verified_filters
821
- end
822
-
823
- def is_filter_relationship?(filter)
824
- filter == _type || _relationships.include?(filter)
825
- end
826
-
827
- def verify_filter(filter, raw, context = nil)
828
- filter_values = []
829
- if raw.present?
830
- begin
831
- filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
832
- rescue CSV::MalformedCSVError
833
- filter_values << raw
834
- end
835
- end
836
-
837
- strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
838
-
839
- if strategy
840
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
841
- values = send(strategy, filter_values, context)
842
- else
843
- values = strategy.call(filter_values, context)
844
- end
845
- [filter, values]
846
- else
847
- if is_filter_relationship?(filter)
848
- verify_relationship_filter(filter, filter_values, context)
849
- else
850
- verify_custom_filter(filter, filter_values, context)
851
- end
852
- end
853
- end
854
-
855
- def key_type(key_type)
856
- @_resource_key_type = key_type
857
- end
858
-
859
- def resource_key_type
860
- @_resource_key_type ||= JSONAPI.configuration.resource_key_type
861
- end
862
-
863
- def verify_key(key, context = nil)
864
- key_type = resource_key_type
865
-
866
- case key_type
867
- when :integer
868
- return if key.nil?
869
- Integer(key)
870
- when :string
871
- return if key.nil?
872
- if key.to_s.include?(',')
873
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
874
- else
875
- key
876
- end
877
- when :uuid
878
- return if key.nil?
879
- if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
880
- key
881
- else
882
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
883
- end
884
- else
885
- key_type.call(key, context)
886
- end
887
- rescue
888
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
889
- end
890
-
891
- # override to allow for key processing and checking
892
- def verify_keys(keys, context = nil)
893
- return keys.collect do |key|
894
- verify_key(key, context)
895
- end
896
- end
897
-
898
- # Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
899
- def verify_custom_filter(filter, value, _context = nil)
900
- [filter, value]
901
- end
902
-
903
- # Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
904
- # relationship logic, such as uuids, multiple keys or permission checks on keys
905
- def verify_relationship_filter(filter, raw, _context = nil)
906
- [filter, raw]
907
- end
908
-
909
- # quasi private class methods
910
- def _attribute_options(attr)
911
- default_attribute_options.merge(@_attributes[attr])
912
- end
913
-
914
- def _has_attribute?(attr)
915
- @_attributes.keys.include?(attr.to_sym)
916
- end
917
-
918
- def _updatable_relationships
919
- @_relationships.map { |key, _relationship| key }
920
- end
921
-
922
- def _relationship(type)
923
- type = type.to_sym
924
- @_relationships[type]
925
- end
926
-
927
- def _model_name
928
- if _abstract
929
- return ''
930
- else
931
- return @_model_name.to_s if defined?(@_model_name)
932
- class_name = self.name
933
- return '' if class_name.nil?
934
- @_model_name = class_name.demodulize.sub(/Resource$/, '')
935
- return @_model_name.to_s
936
- end
937
- end
938
-
939
- def _primary_key
940
- @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
941
- end
942
-
943
- def _cache_field
944
- @_cache_field ||= JSONAPI.configuration.default_resource_cache_field
945
- end
946
-
947
- def _table_name
948
- @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
949
- end
950
-
951
- def _as_parent_key
952
- @_as_parent_key ||= "#{_type.to_s.singularize}_id"
953
- end
954
-
955
- def _allowed_filters
956
- defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
957
- end
958
-
959
- def _paginator
960
- @_paginator ||= JSONAPI.configuration.default_paginator
961
- end
962
-
963
- def paginator(paginator)
964
- @_paginator = paginator
965
- end
966
-
967
- def abstract(val = true)
968
- @abstract = val
969
- end
970
-
971
- def _abstract
972
- @abstract
973
- end
974
-
975
- def immutable(val = true)
976
- @immutable = val
977
- end
978
-
979
- def _immutable
980
- @immutable
981
- end
982
-
983
- def mutable?
984
- !@immutable
985
- end
986
-
987
- def caching(val = true)
988
- @caching = val
989
- end
990
-
991
- def _caching
992
- @caching
993
- end
994
-
995
- def caching?
996
- @caching && !JSONAPI.configuration.resource_cache.nil?
997
- end
998
-
999
- def attribute_caching_context(context)
1000
- nil
1001
- end
1002
-
1003
- def _model_class
1004
- return nil if _abstract
1005
-
1006
- return @model_class if @model_class
1007
-
1008
- model_name = _model_name
1009
- return nil if model_name.to_s.blank?
1010
-
1011
- @model_class = model_name.to_s.safe_constantize
1012
- if @model_class.nil?
1013
- warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
1014
- end
1015
-
1016
- @model_class
1017
- end
1018
-
1019
- def _allowed_filter?(filter)
1020
- !_allowed_filters[filter].nil?
1021
- end
1022
-
1023
- def module_path
1024
- if name == 'JSONAPI::Resource'
1025
- ''
1026
- else
1027
- name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1028
- end
1029
- end
1030
-
1031
- def default_sort
1032
- [{field: 'id', direction: :asc}]
1033
- end
1034
-
1035
- def construct_order_options(sort_params)
1036
- sort_params ||= default_sort
1037
-
1038
- return {} unless sort_params
1039
-
1040
- sort_params.each_with_object({}) do |sort, order_hash|
1041
- field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
1042
- order_hash[field] = sort[:direction]
1043
- end
1044
- end
1045
-
1046
- def _add_relationship(klass, *attrs)
1047
- options = attrs.extract_options!
1048
- options[:parent_resource] = self
1049
-
1050
- attrs.each do |name|
1051
- relationship_name = name.to_sym
1052
- check_reserved_relationship_name(relationship_name)
1053
- check_duplicate_relationship_name(relationship_name)
1054
-
1055
- JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
1056
- .define_relationship_methods(relationship_name.to_sym)
1057
- end
1058
- end
1059
-
1060
- # Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
1061
- def inject_method_definition(name, body)
1062
- define_method(name, body)
1063
- end
1064
-
1065
- def register_relationship(name, relationship_object)
1066
- @_relationships[name] = relationship_object
1067
- end
1068
-
1069
- private
1070
-
1071
- def cached_resources_for(records, serializer, options)
1072
- if records.is_a?(Array) && records.all?{|rec| rec.is_a?(JSONAPI::Resource)}
1073
- resources = records.map{|r| [r.id, r] }.to_h
1074
- elsif self.caching?
1075
- t = _model_class.arel_table
1076
- cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
1077
- resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
1078
- else
1079
- resources = resources_for(records, options[:context]).map{|r| [r.id, r] }.to_h
1080
- end
1081
-
1082
- preload_included_fragments(resources, records, serializer, options)
1083
-
1084
- resources.values
1085
- end
1086
-
1087
- def find_records(filters, options = {})
1088
- context = options[:context]
1089
-
1090
- records = filter_records(filters, options)
1091
-
1092
- sort_criteria = options.fetch(:sort_criteria) { [] }
1093
- order_options = construct_order_options(sort_criteria)
1094
- records = sort_records(records, order_options, context)
1095
-
1096
- records = apply_pagination(records, options[:paginator], order_options)
1097
-
1098
- records
1099
- end
1100
-
1101
- def check_reserved_resource_name(type, name)
1102
- if [:ids, :types, :hrefs, :links].include?(type)
1103
- warn "[NAME COLLISION] `#{name}` is a reserved resource name."
1104
- return
1105
- end
1106
- end
1107
-
1108
- def check_reserved_attribute_name(name)
1109
- # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
1110
- # an attribute method won't be created for it.
1111
- if [:type].include?(name.to_sym)
1112
- warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
1113
- end
1114
- end
1115
-
1116
- def check_reserved_relationship_name(name)
1117
- if [:id, :ids, :type, :types].include?(name.to_sym)
1118
- warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
1119
- end
1120
- end
1121
-
1122
- def check_duplicate_relationship_name(name)
1123
- if _relationships.include?(name.to_sym)
1124
- warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1125
- end
1126
- end
1127
-
1128
- def check_duplicate_attribute_name(name)
1129
- if _attributes.include?(name.to_sym)
1130
- warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1131
- end
1132
- end
1133
-
1134
- def preload_included_fragments(resources, records, serializer, options)
1135
- return if resources.empty?
1136
- res_ids = resources.keys
1137
-
1138
- include_directives = options[:include_directives]
1139
- return unless include_directives
1140
-
1141
- context = options[:context]
1142
-
1143
- # For each association, including indirect associations, find the target record ids.
1144
- # Even if a target class doesn't have caching enabled, we still have to look up
1145
- # and match the target ids here, because we can't use ActiveRecord#includes.
1146
- #
1147
- # Note that `paths` returns partial paths before complete paths, so e.g. the partial
1148
- # fragments for posts.comments will exist before we start working with posts.comments.author
1149
- target_resources = {}
1150
- include_directives.paths.each do |path|
1151
- # If path is [:posts, :comments, :author], then...
1152
- pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
1153
- pluck_attrs << self._model_class.arel_table[self._primary_key]
1154
-
1155
- relation = records
1156
- .except(:limit, :offset, :order)
1157
- .where({_primary_key => res_ids})
1158
-
1159
- # These are updated as we iterate through the association path; afterwards they will
1160
- # refer to the final resource on the path, i.e. the actual resource to find in the cache.
1161
- # So e.g. if path is [:posts, :comments, :author], then after iteration...
1162
- parent_klass = nil # Comment
1163
- klass = self # Person
1164
- relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
1165
- table = nil # people
1166
- assocs_path = [] # [ :posts, :approved_comments, :author ]
1167
- ar_hash = nil # { :posts => { :approved_comments => :author } }
1168
-
1169
- # For each step on the path, figure out what the actual table name/alias in the join
1170
- # will be, and include the primary key of that table in our list of fields to select
1171
- non_polymorphic = true
1172
- path.each do |elem|
1173
- relationship = klass._relationships[elem]
1174
- if relationship.polymorphic
1175
- # Can't preload through a polymorphic belongs_to association, ResourceSerializer
1176
- # will just have to bypass the cache and load the real Resource.
1177
- non_polymorphic = false
1178
- break
1179
- end
1180
- assocs_path << relationship.relation_name(options).to_sym
1181
- # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
1182
- ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
1183
- # We can't just look up the table name from the resource class, because Arel could
1184
- # have used a table alias if the relation includes a self-reference.
1185
- join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node|
1186
- arel_node.is_a?(Arel::Nodes::InnerJoin)
1187
- end
1188
- table = join_source.left
1189
- parent_klass = klass
1190
- klass = relationship.resource_klass
1191
- pluck_attrs << table[klass._primary_key]
1192
- end
1193
- next unless non_polymorphic
1194
-
1195
- # Pre-fill empty hashes for each resource up to the end of the path.
1196
- # This allows us to later distinguish between a preload that returned nothing
1197
- # vs. a preload that never ran.
1198
- prefilling_resources = resources.values
1199
- path.each do |rel_name|
1200
- rel_name = serializer.key_formatter.format(rel_name)
1201
- prefilling_resources.map! do |res|
1202
- res.preloaded_fragments[rel_name] ||= {}
1203
- res.preloaded_fragments[rel_name].values
1204
- end
1205
- prefilling_resources.flatten!(1)
1206
- end
1207
-
1208
- pluck_attrs << table[klass._cache_field] if klass.caching?
1209
- relation = relation.joins(ar_hash)
1210
- if relationship.is_a?(JSONAPI::Relationship::ToMany)
1211
- # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
1212
- # FIXME Should find a better way to reflect on relationship ordering. :-(
1213
- relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders)
1214
- end
1215
-
1216
- # [[post id, comment id, author id, author updated_at], ...]
1217
- id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs)
1218
-
1219
- target_resources[klass.name] ||= {}
1220
-
1221
- if klass.caching?
1222
- sub_cache_ids = id_rows
1223
- .map{|row| row.last(2) }
1224
- .reject{|row| target_resources[klass.name].has_key?(row.first) }
1225
- .uniq
1226
- target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments(
1227
- klass, serializer, context, sub_cache_ids
1228
- )
1229
- else
1230
- sub_res_ids = id_rows
1231
- .map(&:last)
1232
- .reject{|id| target_resources[klass.name].has_key?(id) }
1233
- .uniq
1234
- found = klass.find({klass._primary_key => sub_res_ids}, context: options[:context])
1235
- target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
1236
- end
1237
-
1238
- id_rows.each do |row|
1239
- res = resources[row.first]
1240
- path.each_with_index do |rel_name, index|
1241
- rel_name = serializer.key_formatter.format(rel_name)
1242
- rel_id = row[index+1]
1243
- assoc_rels = res.preloaded_fragments[rel_name]
1244
- if index == path.length - 1
1245
- assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id)
1246
- else
1247
- res = assoc_rels[rel_id]
1248
- end
1249
- end
1250
- end
1251
- end
1252
- end
1253
-
1254
- def pluck_arel_attributes(relation, *attrs)
1255
- conn = relation.connection
1256
- quoted_attrs = attrs.map do |attr|
1257
- quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
1258
- quoted_column = conn.quote_column_name(attr.name)
1259
- "#{quoted_table}.#{quoted_column}"
1260
- end
1261
- relation.pluck(*quoted_attrs)
1262
- end
1263
- end
2
+ class Resource < ActiveRelationResource
3
+ root_resource
1264
4
  end
1265
- end
5
+ end