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.
- checksums.yaml +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +34 -11
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi-resources.rb +8 -3
- data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
- data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
- data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
- data/lib/jsonapi/compiled_json.rb +11 -1
- data/lib/jsonapi/configuration.rb +44 -18
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/exceptions.rb +43 -40
- data/lib/jsonapi/formatter.rb +3 -3
- data/lib/jsonapi/include_directives.rb +2 -45
- data/lib/jsonapi/link_builder.rb +87 -80
- data/lib/jsonapi/operation.rb +16 -5
- data/lib/jsonapi/operation_result.rb +74 -16
- data/lib/jsonapi/processor.rb +233 -112
- data/lib/jsonapi/relationship.rb +77 -53
- data/lib/jsonapi/request_parser.rb +378 -423
- data/lib/jsonapi/resource.rb +224 -524
- data/lib/jsonapi/resource_controller_metal.rb +2 -2
- data/lib/jsonapi/resource_fragment.rb +47 -0
- data/lib/jsonapi/resource_id_tree.rb +112 -0
- data/lib/jsonapi/resource_identity.rb +42 -0
- data/lib/jsonapi/resource_serializer.rb +133 -301
- data/lib/jsonapi/resource_set.rb +108 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +100 -88
- data/lib/jsonapi/routing_ext.rb +21 -43
- metadata +29 -45
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
data/lib/jsonapi/resource.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
require 'jsonapi/callbacks'
|
2
|
-
require 'jsonapi/
|
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)
|
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.
|
279
|
+
relationship = self.class._relationship(relationship_type)
|
295
280
|
|
296
281
|
reflect = reflect_relationship?(relationship, options)
|
297
282
|
|
298
283
|
if reflect
|
299
|
-
|
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,
|
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,
|
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
|
-
|
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)).
|
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,
|
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(
|
441
|
-
subclass.
|
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.
|
465
|
-
|
466
|
-
|
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
|
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
|
495
|
-
|
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
|
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
|
-
|
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 |
|
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 |
|
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
|
-
|
702
|
+
_allowed_sort.keys
|
659
703
|
end
|
660
704
|
|
661
|
-
def
|
662
|
-
|
705
|
+
def sortable_field?(key, context = nil)
|
706
|
+
sortable_fields(context).include? key.to_sym
|
663
707
|
end
|
664
708
|
|
665
|
-
def
|
666
|
-
|
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
|
819
|
-
|
713
|
+
def records(options = {})
|
714
|
+
_model_class.all
|
820
715
|
end
|
821
716
|
|
822
|
-
def
|
823
|
-
|
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 |
|
828
|
-
|
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
|
834
|
-
|
835
|
-
|
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
|
-
|
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
|
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
|
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,
|
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 ||=
|
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
|
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(
|
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 =~
|
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
|
-
|
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
|
-
#
|
1173
|
-
def
|
1174
|
-
|
1175
|
-
|
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
|
-
|
1178
|
-
@_relationships[name] = relationship_object
|
1017
|
+
define_foreign_key_setter(relationship)
|
1179
1018
|
end
|
1180
1019
|
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
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
|
-
|
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
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
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
|
-
|
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
|