jsonapi-resources 0.9.12 → 0.10.0.beta1

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 (36) 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-resources.rb +8 -3
  7. data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
  8. data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
  10. data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
  11. data/lib/jsonapi/compiled_json.rb +11 -1
  12. data/lib/jsonapi/configuration.rb +44 -18
  13. data/lib/jsonapi/error.rb +27 -0
  14. data/lib/jsonapi/exceptions.rb +43 -40
  15. data/lib/jsonapi/formatter.rb +3 -3
  16. data/lib/jsonapi/include_directives.rb +2 -45
  17. data/lib/jsonapi/link_builder.rb +87 -80
  18. data/lib/jsonapi/operation.rb +16 -5
  19. data/lib/jsonapi/operation_result.rb +74 -16
  20. data/lib/jsonapi/processor.rb +233 -112
  21. data/lib/jsonapi/relationship.rb +77 -53
  22. data/lib/jsonapi/request_parser.rb +378 -423
  23. data/lib/jsonapi/resource.rb +224 -524
  24. data/lib/jsonapi/resource_controller_metal.rb +2 -2
  25. data/lib/jsonapi/resource_fragment.rb +47 -0
  26. data/lib/jsonapi/resource_id_tree.rb +112 -0
  27. data/lib/jsonapi/resource_identity.rb +42 -0
  28. data/lib/jsonapi/resource_serializer.rb +133 -301
  29. data/lib/jsonapi/resource_set.rb +108 -0
  30. data/lib/jsonapi/resources/version.rb +1 -1
  31. data/lib/jsonapi/response_document.rb +100 -88
  32. data/lib/jsonapi/routing_ext.rb +21 -43
  33. metadata +29 -45
  34. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  35. data/lib/jsonapi/operation_results.rb +0 -35
  36. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -1,13 +1,10 @@
1
1
  require 'jsonapi/callbacks'
2
- require 'jsonapi/relationship_builder'
2
+ require 'jsonapi/configuration'
3
3
 
4
4
  module JSONAPI
5
5
  class Resource
6
6
  include Callbacks
7
7
 
8
- DEFAULT_ATTRIBUTE_OPTIONS = { format: :default }.freeze
9
- MODULE_PATH_REGEXP = /::[^:]+\Z/.freeze
10
-
11
8
  attr_reader :context
12
9
 
13
10
  define_jsonapi_resources_callbacks :create,
@@ -39,8 +36,12 @@ module JSONAPI
39
36
  _model.public_send(self.class._primary_key)
40
37
  end
41
38
 
39
+ def identity
40
+ JSONAPI::ResourceIdentity.new(self.class, id)
41
+ end
42
+
42
43
  def cache_id
43
- [id, _model.public_send(self.class._cache_field)]
44
+ [id, self.class.hash_cache_field(_model.public_send(self.class._cache_field))]
44
45
  end
45
46
 
46
47
  def is_new?
@@ -121,12 +122,6 @@ module JSONAPI
121
122
  self.class.fields
122
123
  end
123
124
 
124
- # Override this on a resource to customize how the associated records
125
- # are fetched for a model. Particularly helpful for authorization.
126
- def records_for(relation_name)
127
- _model.public_send relation_name
128
- end
129
-
130
125
  def model_error_messages
131
126
  _model.errors.messages
132
127
  end
@@ -172,11 +167,6 @@ module JSONAPI
172
167
  {}
173
168
  end
174
169
 
175
- def preloaded_fragments
176
- # A hash of hashes
177
- @preloaded_fragments ||= Hash.new
178
- end
179
-
180
170
  private
181
171
 
182
172
  def save
@@ -248,14 +238,7 @@ module JSONAPI
248
238
 
249
239
  def _create_to_many_links(relationship_type, relationship_key_values, options)
250
240
  relationship = self.class._relationships[relationship_type]
251
-
252
- # check if relationship_key_values are already members of this relationship
253
241
  relation_name = relationship.relation_name(context: @context)
254
- existing_relations = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_values)
255
- if existing_relations.count > 0
256
- # todo: obscure id so not to leak info
257
- fail JSONAPI::Exceptions::HasManyRelationExists.new(existing_relations.first.id)
258
- end
259
242
 
260
243
  if options[:reflected_source]
261
244
  @model.public_send(relation_name) << options[:reflected_source]._model
@@ -283,7 +266,9 @@ module JSONAPI
283
266
  end
284
267
  @reload_needed = true
285
268
  else
286
- @model.public_send(relation_name) << related_resource._model
269
+ unless @model.public_send(relation_name).include?(related_resource._model)
270
+ @model.public_send(relation_name) << related_resource._model
271
+ end
287
272
  end
288
273
  end
289
274
 
@@ -291,12 +276,15 @@ module JSONAPI
291
276
  end
292
277
 
293
278
  def _replace_to_many_links(relationship_type, relationship_key_values, options)
294
- relationship = self.class._relationships[relationship_type]
279
+ relationship = self.class._relationship(relationship_type)
295
280
 
296
281
  reflect = reflect_relationship?(relationship, options)
297
282
 
298
283
  if reflect
299
- existing = send("#{relationship.foreign_key}")
284
+ existing_rids = self.class.find_related_fragments([identity], relationship_type, options)
285
+
286
+ existing = existing_rids.keys.collect { |rid| rid.id }
287
+
300
288
  to_delete = existing - (relationship_key_values & existing)
301
289
  to_delete.each do |key|
302
290
  _remove_to_many_link(relationship_type, key, reflected_source: self)
@@ -305,26 +293,6 @@ module JSONAPI
305
293
  to_add = relationship_key_values - (relationship_key_values & existing)
306
294
  _create_to_many_links(relationship_type, to_add, {})
307
295
 
308
- @reload_needed = true
309
- elsif relationship.polymorphic?
310
- relationship_key_values.each do |relationship_key_value|
311
- relationship_resource_klass = self.class.resource_for(relationship_key_value[:type])
312
- ids = relationship_key_value[:ids]
313
-
314
- related_records = relationship_resource_klass
315
- .records(options)
316
- .where({relationship_resource_klass._primary_key => ids})
317
-
318
- missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
319
-
320
- if missed_ids.present?
321
- fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
322
- end
323
-
324
- relation_name = relationship.relation_name(context: @context)
325
- @model.send("#{relation_name}") << related_records
326
- end
327
-
328
296
  @reload_needed = true
329
297
  else
330
298
  send("#{relationship.foreign_key}=", relationship_key_values)
@@ -334,7 +302,7 @@ module JSONAPI
334
302
  :completed
335
303
  end
336
304
 
337
- def _replace_to_one_link(relationship_type, relationship_key_value, options)
305
+ def _replace_to_one_link(relationship_type, relationship_key_value, _options)
338
306
  relationship = self.class._relationships[relationship_type]
339
307
 
340
308
  send("#{relationship.foreign_key}=", relationship_key_value)
@@ -343,12 +311,10 @@ module JSONAPI
343
311
  :completed
344
312
  end
345
313
 
346
- def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, options)
314
+ def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, _options)
347
315
  relationship = self.class._relationships[relationship_type.to_sym]
348
316
 
349
- _model.public_send("#{relationship.foreign_key}=", key_value)
350
- _model.public_send("#{relationship.polymorphic_type}=", _model_class_name(key_type))
351
-
317
+ send("#{relationship.foreign_key}=", {type: key_type, id: key_value})
352
318
  @save_needed = true
353
319
 
354
320
  :completed
@@ -375,7 +341,7 @@ module JSONAPI
375
341
 
376
342
  @reload_needed = true
377
343
  else
378
- @model.public_send(relationship.relation_name(context: @context)).delete(key)
344
+ @model.public_send(relationship.relation_name(context: @context)).destroy(key)
379
345
  end
380
346
 
381
347
  :completed
@@ -386,7 +352,7 @@ module JSONAPI
386
352
  fail JSONAPI::Exceptions::RecordNotFound.new(key)
387
353
  end
388
354
 
389
- def _remove_to_one_link(relationship_type, options)
355
+ def _remove_to_one_link(relationship_type, _options)
390
356
  relationship = self.class._relationships[relationship_type]
391
357
 
392
358
  send("#{relationship.foreign_key}=", nil)
@@ -427,24 +393,17 @@ module JSONAPI
427
393
  :completed
428
394
  end
429
395
 
430
- def _model_class_name(key_type)
431
- type_class_name = key_type.to_s.classify
432
- resource = self.class.resource_for(type_class_name)
433
- resource ? resource._model_name.to_s : type_class_name
434
- end
435
-
436
396
  class << self
437
397
  def inherited(subclass)
438
398
  subclass.abstract(false)
439
399
  subclass.immutable(false)
440
- subclass.caching(false)
441
- subclass.singleton(singleton?, (_singleton_options.dup || {}))
442
- subclass.exclude_links(_exclude_links)
400
+ subclass.caching(_caching)
401
+ subclass.paginator(_paginator)
443
402
  subclass._attributes = (_attributes || {}).dup
444
403
 
445
404
  subclass._model_hints = (_model_hints || {}).dup
446
405
 
447
- unless _model_name.empty?
406
+ unless _model_name.empty? || _immutable
448
407
  subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
449
408
  end
450
409
 
@@ -452,18 +411,83 @@ module JSONAPI
452
411
 
453
412
  subclass._allowed_filters = (_allowed_filters || Set.new).dup
454
413
 
414
+ subclass._allowed_sort = _allowed_sort.dup
415
+
455
416
  type = subclass.name.demodulize.sub(/Resource$/, '').underscore
456
417
  subclass._type = type.pluralize.to_sym
457
418
 
458
419
  unless subclass._attributes[:id]
459
- subclass.attribute :id, format: :id
420
+ subclass.attribute :id, format: :id, readonly: true
460
421
  end
461
422
 
462
423
  check_reserved_resource_name(subclass._type, subclass.name)
463
424
 
464
- subclass._routed = false
465
- subclass._warned_missing_route = false
466
- end
425
+ subclass.include JSONAPI.configuration.resource_finder if JSONAPI.configuration.resource_finder
426
+ end
427
+
428
+ # A ResourceFinder is a mixin that adds functionality to find Resources and Resource Fragments
429
+ # to the core Resource class.
430
+ #
431
+ # Resource fragments are a hash with the following format:
432
+ # {
433
+ # identity: <required: a ResourceIdentity>,
434
+ # cache: <optional: the resource's cache value>
435
+ # attributes: <optional: attributes hash for attributes requested - currently unused>
436
+ # related: {
437
+ # <relationship_name>: <ResourceIdentity of a source resource in find_included_fragments>
438
+ # }
439
+ # }
440
+ #
441
+ # begin ResourceFinder Abstract methods
442
+ def find(_filters, _options = {})
443
+ # :nocov:
444
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
445
+ # :nocov:
446
+ end
447
+
448
+ def count(_filters, _options = {})
449
+ # :nocov:
450
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
451
+ # :nocov:
452
+ end
453
+
454
+ def find_by_keys(_keys, _options = {})
455
+ # :nocov:
456
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
457
+ # :nocov:
458
+ end
459
+
460
+ def find_by_key(_key, _options = {})
461
+ # :nocov:
462
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
463
+ # :nocov:
464
+ end
465
+
466
+ def find_fragments(_filters, _options = {})
467
+ # :nocov:
468
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
469
+ # :nocov:
470
+ end
471
+
472
+ def find_included_fragments(_source_rids, _relationship_name, _options = {})
473
+ # :nocov:
474
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
475
+ # :nocov:
476
+ end
477
+
478
+ def find_related_fragments(_source_rids, _relationship_name, _options = {})
479
+ # :nocov:
480
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
481
+ # :nocov:
482
+ end
483
+
484
+ def count_related(_source_rid, _relationship_name, _options = {})
485
+ # :nocov:
486
+ raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.'
487
+ # :nocov:
488
+ end
489
+
490
+ #end ResourceFinder Abstract methods
467
491
 
468
492
  def rebuild_relationships(relationships)
469
493
  original_relationships = relationships.deep_dup
@@ -474,12 +498,13 @@ module JSONAPI
474
498
  original_relationships.each_value do |relationship|
475
499
  options = relationship.options.dup
476
500
  options[:parent_resource] = self
501
+ options[:inverse_relationship] = relationship.inverse_relationship
477
502
  _add_relationship(relationship.class, relationship.name, options)
478
503
  end
479
504
  end
480
505
  end
481
506
 
482
- def resource_for(type)
507
+ def resource_klass_for(type)
483
508
  type = type.underscore
484
509
  type_with_module = type.start_with?(module_path) ? type : module_path + type
485
510
 
@@ -491,8 +516,8 @@ module JSONAPI
491
516
  resource
492
517
  end
493
518
 
494
- def resource_for_model(model)
495
- resource_for(resource_type_for(model))
519
+ def resource_klass_for_model(model)
520
+ resource_klass_for(resource_type_for(model))
496
521
  end
497
522
 
498
523
  def _resource_name_from_type(type)
@@ -508,8 +533,8 @@ module JSONAPI
508
533
  end
509
534
  end
510
535
 
511
- attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
512
- attr_writer :_allowed_filters, :_paginator
536
+ attr_accessor :_attributes, :_relationships, :_type, :_model_hints
537
+ attr_writer :_allowed_filters, :_paginator, :_allowed_sort
513
538
 
514
539
  def create(context)
515
540
  new(create_model, context)
@@ -555,10 +580,40 @@ module JSONAPI
555
580
  define_method "#{attr}=" do |value|
556
581
  @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
557
582
  end unless method_defined?("#{attr}=")
583
+
584
+ if options.fetch(:sortable, true) && !_has_sort?(attr)
585
+ sort attr
586
+ end
587
+ end
588
+
589
+ def attribute_to_model_field(attribute)
590
+ field_name = if attribute == :_cache_field
591
+ _cache_field
592
+ else
593
+ # Note: this will allow the returning of model attributes without a corresponding
594
+ # resource attribute, for example a belongs_to id such as `author_id` or bypassing
595
+ # the delegate.
596
+ attr = @_attributes[attribute]
597
+ attr && attr[:delegate] ? attr[:delegate].to_sym : attribute
598
+ end
599
+ if Rails::VERSION::MAJOR >= 5
600
+ attribute_type = _model_class.attribute_types[field_name.to_s]
601
+ else
602
+ attribute_type = _model_class.column_types[field_name.to_s]
603
+ end
604
+ { name: field_name, type: attribute_type}
605
+ end
606
+
607
+ def cast_to_attribute_type(value, type)
608
+ if Rails::VERSION::MAJOR >= 5
609
+ return type.cast(value)
610
+ else
611
+ return type.type_cast_from_database(value)
612
+ end
558
613
  end
559
614
 
560
615
  def default_attribute_options
561
- DEFAULT_ATTRIBUTE_OPTIONS
616
+ { format: :default }
562
617
  end
563
618
 
564
619
  def relationship(*attrs)
@@ -593,14 +648,7 @@ module JSONAPI
593
648
  _add_relationship(Relationship::ToMany, *attrs)
594
649
  end
595
650
 
596
- # @model_class is inherited from superclass, and this causes some issues:
597
- # ```
598
- # CarResource._model_class #=> Vehicle # it should be Car
599
- # ```
600
- # so in order to invoke the right class from subclasses,
601
- # we should call this method to override it.
602
651
  def model_name(model, options = {})
603
- @model_class = nil
604
652
  @_model_name = model.to_sym
605
653
 
606
654
  model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
@@ -614,19 +662,6 @@ module JSONAPI
614
662
  _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
615
663
  end
616
664
 
617
- def singleton(*attrs)
618
- @_singleton = (!!attrs[0] == attrs[0]) ? attrs[0] : true
619
- @_singleton_options = attrs.extract_options!
620
- end
621
-
622
- def _singleton_options
623
- @_singleton_options ||= {}
624
- end
625
-
626
- def singleton?
627
- @_singleton ||= false
628
- end
629
-
630
665
  def filters(*attrs)
631
666
  @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
632
667
  end
@@ -635,6 +670,15 @@ module JSONAPI
635
670
  @_allowed_filters[attr.to_sym] = args.extract_options!
636
671
  end
637
672
 
673
+ def sort(sorting, options = {})
674
+ self._allowed_sort[sorting.to_sym] = options
675
+ end
676
+
677
+ def sorts(*args)
678
+ options = args.extract_options!
679
+ _allowed_sort.merge!(args.inject({}) { |h, sorting| h[sorting.to_sym] = options.dup; h })
680
+ end
681
+
638
682
  def primary_key(key)
639
683
  @_primary_key = key.to_sym
640
684
  end
@@ -645,243 +689,48 @@ module JSONAPI
645
689
 
646
690
  # Override in your resource to filter the updatable keys
647
691
  def updatable_fields(_context = nil)
648
- _updatable_relationships | _attributes.keys - [:id]
692
+ _updatable_relationships | _updatable_attributes - [:id]
649
693
  end
650
694
 
651
695
  # Override in your resource to filter the creatable keys
652
696
  def creatable_fields(_context = nil)
653
- _updatable_relationships | _attributes.keys - [:id]
697
+ _updatable_relationships | _updatable_attributes
654
698
  end
655
699
 
656
700
  # Override in your resource to filter the sortable keys
657
701
  def sortable_fields(_context = nil)
658
- _attributes.keys
702
+ _allowed_sort.keys
659
703
  end
660
704
 
661
- def fields
662
- _relationships.keys | _attributes.keys
705
+ def sortable_field?(key, context = nil)
706
+ sortable_fields(context).include? key.to_sym
663
707
  end
664
708
 
665
- def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
666
- case model_includes
667
- when Array
668
- return model_includes.map do |value|
669
- resolve_relationship_names_to_relations(resource_klass, value, options)
670
- end
671
- when Hash
672
- model_includes.keys.each do |key|
673
- relationship = resource_klass._relationships[key]
674
- value = model_includes[key]
675
- model_includes.delete(key)
676
- model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
677
- end
678
- return model_includes
679
- when Symbol
680
- relationship = resource_klass._relationships[model_includes]
681
- return relationship.relation_name(options)
682
- end
683
- end
684
-
685
- def apply_includes(records, options = {})
686
- include_directives = options[:include_directives]
687
- if include_directives
688
- model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
689
- records = records.includes(model_includes) if model_includes.present?
690
- end
691
-
692
- records
693
- end
694
-
695
- def apply_pagination(records, paginator, order_options)
696
- records = paginator.apply(records, order_options) if paginator
697
- records
698
- end
699
-
700
- def apply_sort(records, order_options, _context = {})
701
- if order_options.any?
702
- order_options.each_pair do |field, direction|
703
- if field.to_s.include?(".")
704
- *model_names, column_name = field.split(".")
705
-
706
- associations = _lookup_association_chain([records.model.to_s, *model_names])
707
- joins_query = _build_joins([records.model, *associations])
708
-
709
- # _sorting is appended to avoid name clashes with manual joins eg. overridden filters
710
- order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
711
- records = records.joins(joins_query).order(order_by_query)
712
- else
713
- records = records.order(field => direction)
714
- end
715
- end
716
- end
717
-
718
- records
719
- end
720
-
721
- def _lookup_association_chain(model_names)
722
- associations = []
723
- model_names.inject do |prev, current|
724
- association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
725
- assoc.name.to_s.downcase == current.downcase
726
- end
727
- associations << association
728
- association.class_name
729
- end
730
-
731
- associations
732
- end
733
-
734
- def _build_joins(associations)
735
- joins = []
736
-
737
- associations.inject do |prev, current|
738
- joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
739
- current
740
- end
741
- joins.join("\n")
742
- end
743
-
744
- def apply_filter(records, filter, value, options = {})
745
- strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
746
-
747
- if strategy
748
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
749
- send(strategy, records, value, options)
750
- else
751
- strategy.call(records, value, options)
752
- end
753
- else
754
- records.where(filter => value)
755
- end
756
- end
757
-
758
- def apply_filters(records, filters, options = {})
759
- required_includes = []
760
-
761
- if filters
762
- filters.each do |filter, value|
763
- if _relationships.include?(filter)
764
- if _relationships[filter].belongs_to?
765
- records = apply_filter(records, _relationships[filter].foreign_key, value, options)
766
- else
767
- required_includes.push(filter.to_s)
768
- records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
769
- end
770
- else
771
- records = apply_filter(records, filter, value, options)
772
- end
773
- end
774
- end
775
-
776
- if required_includes.any?
777
- records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
778
- end
779
-
780
- records
781
- end
782
-
783
- def apply_included_resources_filters(records, options = {})
784
- include_directives = options[:include_directives]
785
- return records unless include_directives
786
- related_directives = include_directives.include_directives.fetch(:include_related)
787
- related_directives.reduce(records) do |memo, (relationship_name, config)|
788
- relationship = _relationship(relationship_name)
789
- next memo unless relationship && relationship.is_a?(JSONAPI::Relationship::ToMany)
790
- filtering_resource = relationship.resource_klass
791
-
792
- # Don't try to merge where clauses when relation isn't already being joined to query.
793
- next memo unless config[:include_in_join]
794
-
795
- filters = config[:include_filters]
796
- next memo unless filters
797
-
798
- rel_records = filtering_resource.apply_filters(filtering_resource.records(options), filters, options).references(relationship_name)
799
- memo.merge(rel_records)
800
- end
801
- end
802
-
803
- def filter_records(filters, options, records = records(options))
804
- records = apply_filters(records, filters, options)
805
- records = apply_includes(records, options)
806
- apply_included_resources_filters(records, options)
807
- end
808
-
809
- def sort_records(records, order_options, context = {})
810
- apply_sort(records, order_options, context)
811
- end
812
-
813
- # Assumes ActiveRecord's counting. Override if you need a different counting method
814
- def count_records(records)
815
- records.count(:all)
709
+ def fields
710
+ _relationships.keys | _attributes.keys
816
711
  end
817
712
 
818
- def find_count(filters, options = {})
819
- count_records(filter_records(filters, options))
713
+ def records(options = {})
714
+ _model_class.all
820
715
  end
821
716
 
822
- def find(filters, options = {})
823
- resources_for(find_records(filters, options), options[:context])
717
+ def retrieve_records(ids, options = {})
718
+ _model_class.where(_primary_key => ids)
824
719
  end
825
720
 
826
721
  def resources_for(records, context)
827
- records.collect do |model|
828
- resource_class = self.resource_for_model(model)
829
- resource_class.new(model, context)
722
+ records.collect do |record|
723
+ resource_for(record, context)
830
724
  end
831
725
  end
832
726
 
833
- def find_by_keys(keys, options = {})
834
- context = options[:context]
835
- records = records(options)
836
- records = apply_includes(records, options)
837
- models = records.where({_primary_key => keys})
838
- models.collect do |model|
839
- self.resource_for_model(model).new(model, context)
840
- end
841
- end
842
-
843
- def find_serialized_with_caching(filters_or_source, serializer, options = {})
844
- if filters_or_source.is_a?(ActiveRecord::Relation)
845
- records = filters_or_source
846
- elsif _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
847
- records = find_records(filters_or_source, options.except(:include_directives))
848
- else
849
- records = find(filters_or_source, options)
850
- end
851
- cached_resources_for(records, serializer, options)
852
- end
853
-
854
- def find_by_key(key, options = {})
855
- context = options[:context]
856
- records = find_records({_primary_key => key}, options.except(:paginator, :sort_criteria))
857
- model = records.first
858
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
859
- self.resource_for_model(model).new(model, context)
860
- end
861
-
862
- def find_by_key_serialized_with_caching(key, serializer, options = {})
863
- if _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
864
- results = find_serialized_with_caching({_primary_key => key}, serializer, options)
865
- result = results.first
866
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil?
867
- return result
868
- else
869
- resource = find_by_key(key, options)
870
- return cached_resources_for([resource], serializer, options).first
871
- end
872
- end
873
-
874
- # Override this method if you want to customize the relation for
875
- # finder methods (find, find_by_key, find_serialized_with_caching)
876
- def records(_options = {})
877
- _model_class.all
727
+ def resource_for(model_record, context)
728
+ resource_klass = self.resource_klass_for_model(model_record)
729
+ resource_klass.new(model_record, context)
878
730
  end
879
731
 
880
732
  def verify_filters(filters, context = nil)
881
733
  verified_filters = {}
882
-
883
- return verified_filters if filters.nil?
884
-
885
734
  filters.each do |filter, raw_value|
886
735
  verified_filter = verify_filter(filter, raw_value, context)
887
736
  verified_filters[verified_filter[0]] = verified_filter[1]
@@ -906,11 +755,7 @@ module JSONAPI
906
755
  strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
907
756
 
908
757
  if strategy
909
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
910
- values = send(strategy, filter_values, context)
911
- else
912
- values = strategy.call(filter_values, context)
913
- end
758
+ values = call_method_or_proc(strategy, filter_values, context)
914
759
  [filter, values]
915
760
  else
916
761
  if is_filter_relationship?(filter)
@@ -921,6 +766,14 @@ module JSONAPI
921
766
  end
922
767
  end
923
768
 
769
+ def call_method_or_proc(strategy, *args)
770
+ if strategy.is_a?(Symbol) || strategy.is_a?(String)
771
+ send(strategy, *args)
772
+ else
773
+ strategy.call(*args)
774
+ end
775
+ end
776
+
924
777
  def key_type(key_type)
925
778
  @_resource_key_type = key_type
926
779
  end
@@ -929,24 +782,6 @@ module JSONAPI
929
782
  @_resource_key_type ||= JSONAPI.configuration.resource_key_type
930
783
  end
931
784
 
932
- # override to all resolution of masked ids to actual ids. Because singleton routes do not specify the id this
933
- # will be needed to allow lookup of singleton resources. Alternately singleton resources can override
934
- # `verify_key`
935
- def singleton_key(context)
936
- if @_singleton_options && @_singleton_options[:singleton_key]
937
- strategy = @_singleton_options[:singleton_key]
938
- case strategy
939
- when Proc
940
- key = strategy.call(context)
941
- when Symbol, String
942
- key = send(strategy, context)
943
- else
944
- raise "singleton_key must be a proc or function name"
945
- end
946
- end
947
- key
948
- end
949
-
950
785
  def verify_key(key, context = nil)
951
786
  key_type = resource_key_type
952
787
 
@@ -982,12 +817,12 @@ module JSONAPI
982
817
  end
983
818
  end
984
819
 
985
- # Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
820
+ # Either add a custom :verify lambda or override verify_custom_filter to allow for custom filters
986
821
  def verify_custom_filter(filter, value, _context = nil)
987
822
  [filter, value]
988
823
  end
989
824
 
990
- # Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
825
+ # Either add a custom :verify lambda or override verify_relationship_filter to allow for custom
991
826
  # relationship logic, such as uuids, multiple keys or permission checks on keys
992
827
  def verify_relationship_filter(filter, raw, _context = nil)
993
828
  [filter, raw]
@@ -998,12 +833,20 @@ module JSONAPI
998
833
  default_attribute_options.merge(@_attributes[attr])
999
834
  end
1000
835
 
836
+ def _attribute_delegated_name(attr)
837
+ @_attributes.fetch(attr.to_sym, {}).fetch(:delegate, attr)
838
+ end
839
+
1001
840
  def _has_attribute?(attr)
1002
841
  @_attributes.keys.include?(attr.to_sym)
1003
842
  end
1004
843
 
844
+ def _updatable_attributes
845
+ _attributes.map { |key, options| key unless options[:readonly] }.compact
846
+ end
847
+
1005
848
  def _updatable_relationships
1006
- @_relationships.map { |key, _relationship| key }
849
+ @_relationships.map { |key, relationship| key unless relationship.readonly? }.compact
1007
850
  end
1008
851
 
1009
852
  def _relationship(type)
@@ -1024,7 +867,11 @@ module JSONAPI
1024
867
  end
1025
868
 
1026
869
  def _primary_key
1027
- @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
870
+ @_primary_key ||= _default_primary_key
871
+ end
872
+
873
+ def _default_primary_key
874
+ @_default_primary_key ||=_model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
1028
875
  end
1029
876
 
1030
877
  def _cache_field
@@ -1043,6 +890,10 @@ module JSONAPI
1043
890
  defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
1044
891
  end
1045
892
 
893
+ def _allowed_sort
894
+ @_allowed_sort ||= {}
895
+ end
896
+
1046
897
  def _paginator
1047
898
  @_paginator ||= JSONAPI.configuration.default_paginator
1048
899
  end
@@ -1071,31 +922,6 @@ module JSONAPI
1071
922
  !@immutable
1072
923
  end
1073
924
 
1074
- def exclude_links(exclude)
1075
- _resolve_exclude_links(exclude)
1076
- end
1077
-
1078
- def _exclude_links
1079
- @_exclude_links ||= _resolve_exclude_links(JSONAPI.configuration.default_exclude_links)
1080
- end
1081
-
1082
- def exclude_link?(link)
1083
- _exclude_links.include?(link.to_sym)
1084
- end
1085
-
1086
- def _resolve_exclude_links(exclude)
1087
- case exclude
1088
- when :default, "default"
1089
- @_exclude_links = [:self]
1090
- when :none, "none"
1091
- @_exclude_links = []
1092
- when Array
1093
- @_exclude_links = exclude.collect {|link| link.to_sym}
1094
- else
1095
- fail "Invalid exclude_links"
1096
- end
1097
- end
1098
-
1099
925
  def caching(val = true)
1100
926
  @caching = val
1101
927
  end
@@ -1105,13 +931,22 @@ module JSONAPI
1105
931
  end
1106
932
 
1107
933
  def caching?
1108
- @caching && !JSONAPI.configuration.resource_cache.nil?
934
+ if @caching.nil?
935
+ !JSONAPI.configuration.resource_cache.nil? && JSONAPI.configuration.default_caching
936
+ else
937
+ @caching && !JSONAPI.configuration.resource_cache.nil?
938
+ end
1109
939
  end
1110
940
 
1111
- def attribute_caching_context(context)
941
+ def attribute_caching_context(_context)
1112
942
  nil
1113
943
  end
1114
944
 
945
+ # Generate a hashcode from the value to be used as part of the cache lookup
946
+ def hash_cache_field(value)
947
+ value.hash
948
+ end
949
+
1115
950
  def _model_class
1116
951
  return nil if _abstract
1117
952
 
@@ -1132,11 +967,15 @@ module JSONAPI
1132
967
  !_allowed_filters[filter].nil?
1133
968
  end
1134
969
 
970
+ def _has_sort?(sorting)
971
+ !_allowed_sort[sorting.to_sym].nil?
972
+ end
973
+
1135
974
  def module_path
1136
975
  if name == 'JSONAPI::Resource'
1137
976
  ''
1138
977
  else
1139
- name =~ MODULE_PATH_REGEXP ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
978
+ name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1140
979
  end
1141
980
  end
1142
981
 
@@ -1164,52 +1003,44 @@ module JSONAPI
1164
1003
  check_reserved_relationship_name(relationship_name)
1165
1004
  check_duplicate_relationship_name(relationship_name)
1166
1005
 
1167
- JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
1168
- .define_relationship_methods(relationship_name.to_sym)
1006
+ define_relationship_methods(relationship_name.to_sym, klass, options)
1169
1007
  end
1170
1008
  end
1171
1009
 
1172
- # Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
1173
- def inject_method_definition(name, body)
1174
- define_method(name, body)
1175
- end
1010
+ # ResourceBuilder methods
1011
+ def define_relationship_methods(relationship_name, relationship_klass, options)
1012
+ relationship = register_relationship(
1013
+ relationship_name,
1014
+ relationship_klass.new(relationship_name, options)
1015
+ )
1176
1016
 
1177
- def register_relationship(name, relationship_object)
1178
- @_relationships[name] = relationship_object
1017
+ define_foreign_key_setter(relationship)
1179
1018
  end
1180
1019
 
1181
- private
1182
-
1183
- def cached_resources_for(records, serializer, options)
1184
- if records.is_a?(Array) && records.all?{|rec| rec.is_a?(JSONAPI::Resource)}
1185
- resources = records.map{|r| [r.id, r] }.to_h
1186
- elsif self.caching?
1187
- t = _model_class.arel_table
1188
- cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
1189
- resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
1020
+ def define_foreign_key_setter(relationship)
1021
+ if relationship.polymorphic?
1022
+ define_on_resource "#{relationship.foreign_key}=" do |v|
1023
+ _model.method("#{relationship.foreign_key}=").call(v[:id])
1024
+ _model.public_send("#{relationship.polymorphic_type}=", v[:type])
1025
+ end
1190
1026
  else
1191
- resources = resources_for(records, options[:context]).map{|r| [r.id, r] }.to_h
1027
+ define_on_resource "#{relationship.foreign_key}=" do |value|
1028
+ _model.method("#{relationship.foreign_key}=").call(value)
1029
+ end
1192
1030
  end
1193
-
1194
- preload_included_fragments(resources, records, serializer, options)
1195
-
1196
- resources.values
1197
1031
  end
1198
1032
 
1199
- def find_records(filters, options = {})
1200
- context = options[:context]
1201
-
1202
- records = filter_records(filters, options)
1203
-
1204
- sort_criteria = options.fetch(:sort_criteria) { [] }
1205
- order_options = construct_order_options(sort_criteria)
1206
- records = sort_records(records, order_options, context)
1207
-
1208
- records = apply_pagination(records, options[:paginator], order_options)
1033
+ def define_on_resource(method_name, &block)
1034
+ return if method_defined?(method_name)
1035
+ define_method(method_name, block)
1036
+ end
1209
1037
 
1210
- records
1038
+ def register_relationship(name, relationship_object)
1039
+ @_relationships[name] = relationship_object
1211
1040
  end
1212
1041
 
1042
+ private
1043
+
1213
1044
  def check_reserved_resource_name(type, name)
1214
1045
  if [:ids, :types, :hrefs, :links].include?(type)
1215
1046
  warn "[NAME COLLISION] `#{name}` is a reserved resource name."
@@ -1220,7 +1051,7 @@ module JSONAPI
1220
1051
  def check_reserved_attribute_name(name)
1221
1052
  # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
1222
1053
  # an attribute method won't be created for it.
1223
- if [:type].include?(name.to_sym)
1054
+ if [:type, :_cache_field, :cache_field].include?(name.to_sym)
1224
1055
  warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
1225
1056
  end
1226
1057
  end
@@ -1242,137 +1073,6 @@ module JSONAPI
1242
1073
  warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1243
1074
  end
1244
1075
  end
1245
-
1246
- def preload_included_fragments(resources, records, serializer, options)
1247
- return if resources.empty?
1248
- res_ids = resources.keys
1249
-
1250
- include_directives = options[:include_directives]
1251
- return unless include_directives
1252
-
1253
- context = options[:context]
1254
-
1255
- # For each association, including indirect associations, find the target record ids.
1256
- # Even if a target class doesn't have caching enabled, we still have to look up
1257
- # and match the target ids here, because we can't use ActiveRecord#includes.
1258
- #
1259
- # Note that `paths` returns partial paths before complete paths, so e.g. the partial
1260
- # fragments for posts.comments will exist before we start working with posts.comments.author
1261
- target_resources = {}
1262
- include_directives.paths.each do |path|
1263
- # If path is [:posts, :comments, :author], then...
1264
- pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
1265
- pluck_attrs << self._model_class.arel_table[self._primary_key]
1266
-
1267
- relation = records
1268
- .except(:limit, :offset, :order)
1269
- .where({_primary_key => res_ids})
1270
-
1271
- # These are updated as we iterate through the association path; afterwards they will
1272
- # refer to the final resource on the path, i.e. the actual resource to find in the cache.
1273
- # So e.g. if path is [:posts, :comments, :author], then after iteration...
1274
- parent_klass = nil # Comment
1275
- klass = self # Person
1276
- relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
1277
- table = nil # people
1278
- assocs_path = [] # [ :posts, :approved_comments, :author ]
1279
- ar_hash = nil # { :posts => { :approved_comments => :author } }
1280
-
1281
- # For each step on the path, figure out what the actual table name/alias in the join
1282
- # will be, and include the primary key of that table in our list of fields to select
1283
- non_polymorphic = true
1284
- path.each do |elem|
1285
- relationship = klass._relationships[elem]
1286
- if relationship.polymorphic
1287
- # Can't preload through a polymorphic belongs_to association, ResourceSerializer
1288
- # will just have to bypass the cache and load the real Resource.
1289
- non_polymorphic = false
1290
- break
1291
- end
1292
- assocs_path << relationship.relation_name(options).to_sym
1293
- # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
1294
- ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
1295
- # We can't just look up the table name from the resource class, because Arel could
1296
- # have used a table alias if the relation includes a self-reference.
1297
- join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node|
1298
- arel_node.is_a?(Arel::Nodes::InnerJoin)
1299
- end
1300
- table = join_source.left
1301
- parent_klass = klass
1302
- klass = relationship.resource_klass
1303
- pluck_attrs << table[klass._primary_key]
1304
- end
1305
- next unless non_polymorphic
1306
-
1307
- # Pre-fill empty hashes for each resource up to the end of the path.
1308
- # This allows us to later distinguish between a preload that returned nothing
1309
- # vs. a preload that never ran.
1310
- prefilling_resources = resources.values
1311
- path.each do |rel_name|
1312
- rel_name = serializer.key_formatter.format(rel_name)
1313
- prefilling_resources.map! do |res|
1314
- res.preloaded_fragments[rel_name] ||= {}
1315
- res.preloaded_fragments[rel_name].values
1316
- end
1317
- prefilling_resources.flatten!(1)
1318
- end
1319
-
1320
- pluck_attrs << table[klass._cache_field] if klass.caching?
1321
- relation = relation.joins(ar_hash)
1322
- if relationship.is_a?(JSONAPI::Relationship::ToMany)
1323
- # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
1324
- # FIXME Should find a better way to reflect on relationship ordering. :-(
1325
- relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders)
1326
- end
1327
-
1328
- # [[post id, comment id, author id, author updated_at], ...]
1329
- id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs)
1330
-
1331
- target_resources[klass.name] ||= {}
1332
-
1333
- if klass.caching?
1334
- sub_cache_ids = id_rows
1335
- .map{|row| row.last(2) }
1336
- .reject{|row| target_resources[klass.name].has_key?(row.first) }
1337
- .uniq
1338
- target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments(
1339
- klass, serializer, context, sub_cache_ids
1340
- )
1341
- else
1342
- sub_res_ids = id_rows
1343
- .map(&:last)
1344
- .reject{|id| target_resources[klass.name].has_key?(id) }
1345
- .uniq
1346
- found = klass.find({klass._primary_key => sub_res_ids}, context: options[:context])
1347
- target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
1348
- end
1349
-
1350
- id_rows.each do |row|
1351
- res = resources[row.first]
1352
- path.each_with_index do |rel_name, index|
1353
- rel_name = serializer.key_formatter.format(rel_name)
1354
- rel_id = row[index+1]
1355
- assoc_rels = res.preloaded_fragments[rel_name]
1356
- if index == path.length - 1
1357
- association_res = target_resources[klass.name].fetch(rel_id, nil)
1358
- assoc_rels[rel_id] = association_res if association_res
1359
- else
1360
- res = assoc_rels[rel_id]
1361
- end
1362
- end
1363
- end
1364
- end
1365
- end
1366
-
1367
- def pluck_arel_attributes(relation, *attrs)
1368
- conn = relation.connection
1369
- quoted_attrs = attrs.map do |attr|
1370
- quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
1371
- quoted_column = conn.quote_column_name(attr.name)
1372
- Arel.sql("#{quoted_table}.#{quoted_column}")
1373
- end
1374
- relation.pluck(*quoted_attrs)
1375
- end
1376
1076
  end
1377
1077
  end
1378
1078
  end