jsonapi-resources 0.9.11 → 0.10.7

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 (45) hide show
  1. checksums.yaml +4 -4
  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 +303 -0
  8. data/lib/jsonapi/active_relation_resource.rb +884 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -106
  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 +57 -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 -75
  19. data/lib/jsonapi/link_builder.rb +16 -19
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +73 -15
  22. data/lib/jsonapi/paginator.rb +17 -0
  23. data/lib/jsonapi/path.rb +43 -0
  24. data/lib/jsonapi/path_segment.rb +76 -0
  25. data/lib/jsonapi/processor.rb +246 -111
  26. data/lib/jsonapi/relationship.rb +117 -18
  27. data/lib/jsonapi/request_parser.rb +383 -396
  28. data/lib/jsonapi/resource.rb +3 -1376
  29. data/lib/jsonapi/resource_controller_metal.rb +5 -2
  30. data/lib/jsonapi/resource_fragment.rb +47 -0
  31. data/lib/jsonapi/resource_id_tree.rb +112 -0
  32. data/lib/jsonapi/resource_identity.rb +42 -0
  33. data/lib/jsonapi/resource_serializer.rb +124 -286
  34. data/lib/jsonapi/resource_set.rb +176 -0
  35. data/lib/jsonapi/resources/railtie.rb +9 -0
  36. data/lib/jsonapi/resources/version.rb +1 -1
  37. data/lib/jsonapi/response_document.rb +104 -87
  38. data/lib/jsonapi/routing_ext.rb +19 -21
  39. data/lib/jsonapi-resources.rb +20 -4
  40. data/lib/tasks/check_upgrade.rake +52 -0
  41. metadata +36 -33
  42. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  43. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  44. data/lib/jsonapi/operation_results.rb +0 -35
  45. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -0,0 +1,884 @@
1
+ module JSONAPI
2
+ class ActiveRelationResource < BasicResource
3
+ root_resource
4
+
5
+ class << self
6
+ # Finds Resources using the `filters`. Pagination and sort options are used when provided
7
+ #
8
+ # @param filters [Hash] the filters hash
9
+ # @option options [Hash] :context The context of the request, set in the controller
10
+ # @option options [Hash] :sort_criteria The `sort criteria`
11
+ # @option options [Hash] :include_directives The `include_directives`
12
+ #
13
+ # @return [Array<Resource>] the Resource instances matching the filters, sorting and pagination rules.
14
+ def find(filters, options = {})
15
+ sort_criteria = options.fetch(:sort_criteria) { [] }
16
+
17
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
18
+ filters: filters,
19
+ sort_criteria: sort_criteria)
20
+
21
+ paginator = options[:paginator]
22
+
23
+ records = apply_request_settings_to_records(records: records(options),
24
+ sort_criteria: sort_criteria,filters: filters,
25
+ join_manager: join_manager,
26
+ paginator: paginator,
27
+ options: options)
28
+
29
+ resources_for(records, options[:context])
30
+ end
31
+
32
+ # Counts Resources found using the `filters`
33
+ #
34
+ # @param filters [Hash] the filters hash
35
+ # @option options [Hash] :context The context of the request, set in the controller
36
+ #
37
+ # @return [Integer] the count
38
+ def count(filters, options = {})
39
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
40
+ filters: filters)
41
+
42
+ records = apply_request_settings_to_records(records: records(options),
43
+ filters: filters,
44
+ join_manager: join_manager,
45
+ options: options)
46
+
47
+ count_records(records)
48
+ end
49
+
50
+ # Returns the single Resource identified by `key`
51
+ #
52
+ # @param key the primary key of the resource to find
53
+ # @option options [Hash] :context The context of the request, set in the controller
54
+ def find_by_key(key, options = {})
55
+ record = find_record_by_key(key, options)
56
+ resource_for(record, options[:context])
57
+ end
58
+
59
+ # Returns an array of Resources identified by the `keys` array
60
+ #
61
+ # @param keys [Array<key>] Array of primary keys to find resources for
62
+ # @option options [Hash] :context The context of the request, set in the controller
63
+ def find_by_keys(keys, options = {})
64
+ records = find_records_by_keys(keys, options)
65
+ resources_for(records, options[:context])
66
+ end
67
+
68
+ # Returns an array of Resources identified by the `keys` array. The resources are not filtered as this
69
+ # will have been done in a prior step
70
+ #
71
+ # @param keys [Array<key>] Array of primary keys to find resources for
72
+ # @option options [Hash] :context The context of the request, set in the controller
73
+ def find_to_populate_by_keys(keys, options = {})
74
+ records = records_for_populate(options).where(_primary_key => keys)
75
+ resources_for(records, options[:context])
76
+ end
77
+
78
+ # Finds Resource fragments using the `filters`. Pagination and sort options are used when provided.
79
+ # Retrieving the ResourceIdentities and attributes does not instantiate a model instance.
80
+ # Note: This is incompatible with Polymorphic resources (which are going to come from two separate tables)
81
+ #
82
+ # @param filters [Hash] the filters hash
83
+ # @option options [Hash] :context The context of the request, set in the controller
84
+ # @option options [Hash] :sort_criteria The `sort criteria`
85
+ # @option options [Hash] :include_directives The `include_directives`
86
+ # @option options [Hash] :attributes Additional fields to be retrieved.
87
+ # @option options [Boolean] :cache Return the resources' cache field
88
+ #
89
+ # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, attributes: => {name => value}}}]
90
+ # the ResourceInstances matching the filters, sorting, and pagination rules along with any request
91
+ # additional_field values
92
+ def find_fragments(filters, options = {})
93
+ include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
94
+ resource_klass = self
95
+ linkage_relationships = to_one_relationships_for_linkage(include_directives[:include_related])
96
+
97
+ sort_criteria = options.fetch(:sort_criteria) { [] }
98
+
99
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: resource_klass,
100
+ source_relationship: nil,
101
+ relationships: linkage_relationships,
102
+ sort_criteria: sort_criteria,
103
+ filters: filters)
104
+
105
+ paginator = options[:paginator]
106
+
107
+ records = apply_request_settings_to_records(records: records(options),
108
+ filters: filters,
109
+ sort_criteria: sort_criteria,
110
+ paginator: paginator,
111
+ join_manager: join_manager,
112
+ options: options)
113
+
114
+ # This alias is going to be resolve down to the model's table name and will not actually be an alias
115
+ resource_table_alias = resource_klass._table_name
116
+
117
+ pluck_fields = [sql_field_with_alias(resource_table_alias, resource_klass._primary_key)]
118
+
119
+ cache_field = attribute_to_model_field(:_cache_field) if options[:cache]
120
+ if cache_field
121
+ pluck_fields << sql_field_with_alias(resource_table_alias, cache_field[:name])
122
+ end
123
+
124
+ linkage_fields = []
125
+
126
+ linkage_relationships.each do |name|
127
+ linkage_relationship = resource_klass._relationship(name)
128
+
129
+ if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
130
+ linkage_relationship.resource_types.each do |resource_type|
131
+ klass = resource_klass_for(resource_type)
132
+ linkage_fields << {relationship_name: name, resource_klass: klass}
133
+
134
+ linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
135
+ primary_key = klass._primary_key
136
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
137
+ end
138
+ else
139
+ klass = linkage_relationship.resource_klass
140
+ linkage_fields << {relationship_name: name, resource_klass: klass}
141
+
142
+ linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
143
+ primary_key = klass._primary_key
144
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
145
+ end
146
+ end
147
+
148
+ model_fields = {}
149
+ attributes = options[:attributes]
150
+ attributes.try(:each) do |attribute|
151
+ model_field = resource_klass.attribute_to_model_field(attribute)
152
+ model_fields[attribute] = model_field
153
+ pluck_fields << sql_field_with_alias(resource_table_alias, model_field[:name])
154
+ end
155
+
156
+ sort_fields = options.dig(:_relation_helper_options, :sort_fields)
157
+ sort_fields.try(:each) do |field|
158
+ pluck_fields << Arel.sql(field)
159
+ end
160
+
161
+ fragments = {}
162
+ rows = records.pluck(*pluck_fields)
163
+ rows.each do |row|
164
+ rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0])
165
+
166
+ fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
167
+ attributes_offset = 1
168
+
169
+ if cache_field
170
+ fragments[rid].cache = cast_to_attribute_type(row[1], cache_field[:type])
171
+ attributes_offset+= 1
172
+ end
173
+
174
+ linkage_fields.each do |linkage_field_details|
175
+ fragments[rid].initialize_related(linkage_field_details[:relationship_name])
176
+ related_id = row[attributes_offset]
177
+ if related_id
178
+ related_rid = JSONAPI::ResourceIdentity.new(linkage_field_details[:resource_klass], related_id)
179
+ fragments[rid].add_related_identity(linkage_field_details[:relationship_name], related_rid)
180
+ end
181
+ attributes_offset+= 1
182
+ end
183
+
184
+ model_fields.each_with_index do |k, idx|
185
+ fragments[rid].attributes[k[0]]= cast_to_attribute_type(row[idx + attributes_offset], k[1][:type])
186
+ end
187
+ end
188
+
189
+ if JSONAPI.configuration.warn_on_performance_issues && (rows.length > fragments.length)
190
+ warn "Performance issue detected: `#{self.name.to_s}.records` returned non-normalized results in `#{self.name.to_s}.find_fragments`."
191
+ end
192
+
193
+ fragments
194
+ end
195
+
196
+ # Finds Resource Fragments related to the source resources through the specified relationship
197
+ #
198
+ # @param source_rids [Array<ResourceIdentity>] The resources to find related ResourcesIdentities for
199
+ # @param relationship_name [String | Symbol] The name of the relationship
200
+ # @option options [Hash] :context The context of the request, set in the controller
201
+ # @option options [Hash] :attributes Additional fields to be retrieved.
202
+ # @option options [Boolean] :cache Return the resources' cache field
203
+ #
204
+ # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, attributes: => {name => value}, related: {relationship_name: [] }}}]
205
+ # the ResourceInstances matching the filters, sorting, and pagination rules along with any request
206
+ # additional_field values
207
+ def find_related_fragments(source_rids, relationship_name, options = {})
208
+ relationship = _relationship(relationship_name)
209
+
210
+ if relationship.polymorphic? # && relationship.foreign_key_on == :self
211
+ find_related_polymorphic_fragments(source_rids, relationship, options, false)
212
+ else
213
+ find_related_monomorphic_fragments(source_rids, relationship, options, false)
214
+ end
215
+ end
216
+
217
+ def find_included_fragments(source_rids, relationship_name, options)
218
+ relationship = _relationship(relationship_name)
219
+
220
+ if relationship.polymorphic? # && relationship.foreign_key_on == :self
221
+ find_related_polymorphic_fragments(source_rids, relationship, options, true)
222
+ else
223
+ find_related_monomorphic_fragments(source_rids, relationship, options, true)
224
+ end
225
+ end
226
+
227
+ # Counts Resources related to the source resource through the specified relationship
228
+ #
229
+ # @param source_rid [ResourceIdentity] Source resource identifier
230
+ # @param relationship_name [String | Symbol] The name of the relationship
231
+ # @option options [Hash] :context The context of the request, set in the controller
232
+ #
233
+ # @return [Integer] the count
234
+ def count_related(source_rid, relationship_name, options = {})
235
+ relationship = _relationship(relationship_name)
236
+ related_klass = relationship.resource_klass
237
+
238
+ filters = options.fetch(:filters, {})
239
+
240
+ # Joins in this case are related to the related_klass
241
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
242
+ source_relationship: relationship,
243
+ filters: filters)
244
+
245
+ records = apply_request_settings_to_records(records: records(options),
246
+ resource_klass: related_klass,
247
+ primary_keys: source_rid.id,
248
+ join_manager: join_manager,
249
+ filters: filters,
250
+ options: options)
251
+
252
+ related_alias = join_manager.join_details_by_relationship(relationship)[:alias]
253
+
254
+ records = records.select(Arel.sql("#{concat_table_field(related_alias, related_klass._primary_key)}"))
255
+
256
+ count_records(records)
257
+ end
258
+
259
+ # This resource class (ActiveRelationResource) uses an `ActiveRecord::Relation` as the starting point for
260
+ # retrieving models. From this relation filters, sorts and joins are applied as needed.
261
+ # Depending on which phase of the request processing different `records` methods will be called, giving the user
262
+ # the opportunity to override them differently for performance and security reasons.
263
+
264
+ # begin `records`methods
265
+
266
+ # Base for the `records` methods that follow and is not directly used for accessing model data by this class.
267
+ # Overriding this method gives a single place to affect the `ActiveRecord::Relation` used for the resource.
268
+ #
269
+ # @option options [Hash] :context The context of the request, set in the controller
270
+ #
271
+ # @return [ActiveRecord::Relation]
272
+ def records_base(_options = {})
273
+ _model_class.all
274
+ end
275
+
276
+ # The `ActiveRecord::Relation` used for finding user requested models. This may be overridden to enforce
277
+ # permissions checks on the request.
278
+ #
279
+ # @option options [Hash] :context The context of the request, set in the controller
280
+ #
281
+ # @return [ActiveRecord::Relation]
282
+ def records(options = {})
283
+ records_base(options)
284
+ end
285
+
286
+ # The `ActiveRecord::Relation` used for populating the ResourceSet. Only resources that have been previously
287
+ # identified through the `records` method will be accessed. Thus it should not be necessary to reapply permissions
288
+ # checks. However if the model needs to include other models adding `includes` is appropriate
289
+ #
290
+ # @option options [Hash] :context The context of the request, set in the controller
291
+ #
292
+ # @return [ActiveRecord::Relation]
293
+ def records_for_populate(options = {})
294
+ records_base(options)
295
+ end
296
+
297
+ # The `ActiveRecord::Relation` used for the finding related resources. Only resources that have been previously
298
+ # identified through the `records` method will be accessed and used as the basis to find related resources. Thus
299
+ # it should not be necessary to reapply permissions checks.
300
+ #
301
+ # @option options [Hash] :context The context of the request, set in the controller
302
+ #
303
+ # @return [ActiveRecord::Relation]
304
+ def records_for_source_to_related(options = {})
305
+ records_base(options)
306
+ end
307
+
308
+ # end `records` methods
309
+
310
+ def apply_join(records:, relationship:, resource_type:, join_type:, options:)
311
+ if relationship.polymorphic? && relationship.belongs_to?
312
+ case join_type
313
+ when :inner
314
+ records = records.joins(resource_type.to_s.singularize.to_sym)
315
+ when :left
316
+ records = records.joins_left(resource_type.to_s.singularize.to_sym)
317
+ end
318
+ else
319
+ relation_name = relationship.relation_name(options)
320
+ case join_type
321
+ when :inner
322
+ records = records.joins(relation_name)
323
+ when :left
324
+ records = records.joins_left(relation_name)
325
+ end
326
+ end
327
+
328
+ if relationship.use_related_resource_records_for_joins
329
+ records = records.merge(self.records(options))
330
+ end
331
+
332
+ records
333
+ end
334
+
335
+ def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
336
+ records = relationship.parent_resource.records_for_source_to_related(options)
337
+ strategy = relationship.options[:apply_join]
338
+
339
+ if strategy
340
+ records = call_method_or_proc(strategy, records, relationship, resource_type, join_type, options)
341
+ else
342
+ records = apply_join(records: records,
343
+ relationship: relationship,
344
+ resource_type: resource_type,
345
+ join_type: join_type,
346
+ options: options)
347
+ end
348
+
349
+ records
350
+ end
351
+
352
+ def join_relationship(records:, relationship:, resource_type: nil, join_type: :inner, options: {})
353
+ relationship_records = relationship_records(relationship: relationship,
354
+ join_type: join_type,
355
+ resource_type: resource_type,
356
+ options: options)
357
+ records.merge(relationship_records)
358
+ end
359
+
360
+ protected
361
+
362
+ def to_one_relationships_for_linkage(include_related)
363
+ include_related ||= {}
364
+ relationships = []
365
+ _relationships.each do |name, relationship|
366
+ if relationship.is_a?(JSONAPI::Relationship::ToOne) && !include_related.has_key?(name) && relationship.include_optional_linkage_data?
367
+ relationships << name
368
+ end
369
+ end
370
+ relationships
371
+ end
372
+
373
+ def find_record_by_key(key, options = {})
374
+ record = apply_request_settings_to_records(records: records(options), primary_keys: key, options: options).first
375
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if record.nil?
376
+ record
377
+ end
378
+
379
+ def find_records_by_keys(keys, options = {})
380
+ apply_request_settings_to_records(records: records(options), primary_keys: keys, options: options)
381
+ end
382
+
383
+ def find_related_monomorphic_fragments(source_rids, relationship, options, connect_source_identity)
384
+ filters = options.fetch(:filters, {})
385
+ source_ids = source_rids.collect {|rid| rid.id}
386
+
387
+ include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
388
+ resource_klass = relationship.resource_klass
389
+ linkage_relationships = resource_klass.to_one_relationships_for_linkage(include_directives[:include_related])
390
+
391
+ sort_criteria = []
392
+ options[:sort_criteria].try(:each) do |sort|
393
+ field = sort[:field].to_s == 'id' ? resource_klass._primary_key : sort[:field]
394
+ sort_criteria << { field: field, direction: sort[:direction] }
395
+ end
396
+
397
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
398
+ source_relationship: relationship,
399
+ relationships: linkage_relationships,
400
+ sort_criteria: sort_criteria,
401
+ filters: filters)
402
+
403
+ paginator = options[:paginator]
404
+
405
+ records = apply_request_settings_to_records(records: records_for_source_to_related(options),
406
+ resource_klass: resource_klass,
407
+ sort_criteria: sort_criteria,
408
+ primary_keys: source_ids,
409
+ paginator: paginator,
410
+ filters: filters,
411
+ join_manager: join_manager,
412
+ options: options)
413
+
414
+ resource_table_alias = join_manager.join_details_by_relationship(relationship)[:alias]
415
+
416
+ pluck_fields = [
417
+ Arel.sql("#{_table_name}.#{_primary_key} AS \"source_id\""),
418
+ sql_field_with_alias(resource_table_alias, resource_klass._primary_key)
419
+ ]
420
+
421
+ cache_field = resource_klass.attribute_to_model_field(:_cache_field) if options[:cache]
422
+ if cache_field
423
+ pluck_fields << sql_field_with_alias(resource_table_alias, cache_field[:name])
424
+ end
425
+
426
+ linkage_fields = []
427
+
428
+ linkage_relationships.each do |name|
429
+ linkage_relationship = resource_klass._relationship(name)
430
+
431
+ if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
432
+ linkage_relationship.resource_types.each do |resource_type|
433
+ klass = resource_klass_for(resource_type)
434
+ linkage_fields << {relationship_name: name, resource_klass: klass}
435
+
436
+ linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
437
+ primary_key = klass._primary_key
438
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
439
+ end
440
+ else
441
+ klass = linkage_relationship.resource_klass
442
+ linkage_fields << {relationship_name: name, resource_klass: klass}
443
+
444
+ linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
445
+ primary_key = klass._primary_key
446
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
447
+ end
448
+ end
449
+
450
+ model_fields = {}
451
+ attributes = options[:attributes]
452
+ attributes.try(:each) do |attribute|
453
+ model_field = resource_klass.attribute_to_model_field(attribute)
454
+ model_fields[attribute] = model_field
455
+ pluck_fields << sql_field_with_alias(resource_table_alias, model_field[:name])
456
+ end
457
+
458
+ sort_fields = options.dig(:_relation_helper_options, :sort_fields)
459
+ sort_fields.try(:each) do |field|
460
+ pluck_fields << Arel.sql(field)
461
+ end
462
+
463
+ fragments = {}
464
+ rows = records.distinct.pluck(*pluck_fields)
465
+ rows.each do |row|
466
+ rid = JSONAPI::ResourceIdentity.new(resource_klass, row[1])
467
+
468
+ fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
469
+
470
+ attributes_offset = 2
471
+
472
+ if cache_field
473
+ fragments[rid].cache = cast_to_attribute_type(row[attributes_offset], cache_field[:type])
474
+ attributes_offset+= 1
475
+ end
476
+
477
+ model_fields.each_with_index do |k, idx|
478
+ fragments[rid].add_attribute(k[0], cast_to_attribute_type(row[idx + attributes_offset], k[1][:type]))
479
+ attributes_offset+= 1
480
+ end
481
+
482
+ source_rid = JSONAPI::ResourceIdentity.new(self, row[0])
483
+
484
+ fragments[rid].add_related_from(source_rid)
485
+
486
+ linkage_fields.each do |linkage_field|
487
+ fragments[rid].initialize_related(linkage_field[:relationship_name])
488
+ related_id = row[attributes_offset]
489
+ if related_id
490
+ related_rid = JSONAPI::ResourceIdentity.new(linkage_field[:resource_klass], related_id)
491
+ fragments[rid].add_related_identity(linkage_field[:relationship_name], related_rid)
492
+ end
493
+ attributes_offset+= 1
494
+ end
495
+
496
+ if connect_source_identity
497
+ related_relationship = resource_klass._relationships[relationship.inverse_relationship]
498
+ if related_relationship
499
+ fragments[rid].add_related_identity(related_relationship.name, source_rid)
500
+ end
501
+ end
502
+ end
503
+
504
+ fragments
505
+ end
506
+
507
+ # Gets resource identities where the related resource is polymorphic and the resource type and id
508
+ # are stored on the primary resources. Cache fields will always be on the related resources.
509
+ def find_related_polymorphic_fragments(source_rids, relationship, options, connect_source_identity)
510
+ filters = options.fetch(:filters, {})
511
+ source_ids = source_rids.collect {|rid| rid.id}
512
+
513
+ resource_klass = relationship.resource_klass
514
+ include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
515
+
516
+ linkage_relationships = []
517
+
518
+ resource_types = relationship.resource_types
519
+
520
+ resource_types.each do |resource_type|
521
+ related_resource_klass = resource_klass_for(resource_type)
522
+ relationships = related_resource_klass.to_one_relationships_for_linkage(include_directives[:include_related])
523
+ relationships.each do |r|
524
+ linkage_relationships << "##{resource_type}.#{r}"
525
+ end
526
+ end
527
+
528
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
529
+ source_relationship: relationship,
530
+ relationships: linkage_relationships,
531
+ filters: filters)
532
+
533
+ paginator = options[:paginator]
534
+
535
+ # Note: We will sort by the source table. Without using unions we can't sort on a polymorphic relationship
536
+ # in any manner that makes sense
537
+ records = apply_request_settings_to_records(records: records_for_source_to_related(options),
538
+ resource_klass: resource_klass,
539
+ sort_primary: true,
540
+ primary_keys: source_ids,
541
+ paginator: paginator,
542
+ filters: filters,
543
+ join_manager: join_manager,
544
+ options: options)
545
+
546
+ primary_key = concat_table_field(_table_name, _primary_key)
547
+ related_key = concat_table_field(_table_name, relationship.foreign_key)
548
+ related_type = concat_table_field(_table_name, relationship.polymorphic_type)
549
+
550
+ pluck_fields = [
551
+ Arel.sql("#{primary_key} AS #{alias_table_field(_table_name, _primary_key)}"),
552
+ Arel.sql("#{related_key} AS #{alias_table_field(_table_name, relationship.foreign_key)}"),
553
+ Arel.sql("#{related_type} AS #{alias_table_field(_table_name, relationship.polymorphic_type)}")
554
+ ]
555
+
556
+ # Get the additional fields from each relation. There's a limitation that the fields must exist in each relation
557
+
558
+ relation_positions = {}
559
+ relation_index = pluck_fields.length
560
+
561
+ attributes = options.fetch(:attributes, [])
562
+
563
+ # Add resource specific fields
564
+ if resource_types.nil? || resource_types.length == 0
565
+ # :nocov:
566
+ warn "No resource types found for polymorphic relationship."
567
+ # :nocov:
568
+ else
569
+ resource_types.try(:each) do |type|
570
+ related_klass = resource_klass_for(type.to_s)
571
+
572
+ cache_field = related_klass.attribute_to_model_field(:_cache_field) if options[:cache]
573
+
574
+ table_alias = join_manager.source_join_details(type)[:alias]
575
+
576
+ cache_offset = relation_index
577
+ if cache_field
578
+ pluck_fields << sql_field_with_alias(table_alias, cache_field[:name])
579
+ relation_index+= 1
580
+ end
581
+
582
+ model_fields = {}
583
+ field_offset = relation_index
584
+ attributes.try(:each) do |attribute|
585
+ model_field = related_klass.attribute_to_model_field(attribute)
586
+ model_fields[attribute] = model_field
587
+ pluck_fields << sql_field_with_alias(table_alias, model_field[:name])
588
+ relation_index+= 1
589
+ end
590
+
591
+ model_offset = relation_index
592
+ model_fields.each do |_k, v|
593
+ pluck_fields << Arel.sql("#{concat_table_field(table_alias, v[:name])}")
594
+ relation_index+= 1
595
+ end
596
+
597
+ relation_positions[type] = {relation_klass: related_klass,
598
+ cache_field: cache_field,
599
+ cache_offset: cache_offset,
600
+ model_fields: model_fields,
601
+ model_offset: model_offset,
602
+ field_offset: field_offset}
603
+ end
604
+ end
605
+
606
+ # Add to_one linkage fields
607
+ linkage_fields = []
608
+ linkage_offset = relation_index
609
+
610
+ linkage_relationships.each do |linkage_relationship_path|
611
+ path = JSONAPI::Path.new(resource_klass: self,
612
+ path_string: "#{relationship.name}#{linkage_relationship_path}",
613
+ ensure_default_field: false)
614
+
615
+ linkage_relationship = path.segments[-1].relationship
616
+
617
+ if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
618
+ linkage_relationship.resource_types.each do |resource_type|
619
+ klass = resource_klass_for(resource_type)
620
+ linkage_fields << {relationship: linkage_relationship, resource_klass: klass}
621
+
622
+ linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
623
+ primary_key = klass._primary_key
624
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
625
+ end
626
+ else
627
+ klass = linkage_relationship.resource_klass
628
+ linkage_fields << {relationship: linkage_relationship, resource_klass: klass}
629
+
630
+ linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
631
+ primary_key = klass._primary_key
632
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
633
+ end
634
+ end
635
+
636
+ rows = records.distinct.pluck(*pluck_fields)
637
+
638
+ related_fragments = {}
639
+
640
+ rows.each do |row|
641
+ unless row[1].nil? || row[2].nil?
642
+ related_klass = resource_klass_for(row[2])
643
+
644
+ rid = JSONAPI::ResourceIdentity.new(related_klass, row[1])
645
+ related_fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
646
+
647
+ source_rid = JSONAPI::ResourceIdentity.new(self, row[0])
648
+ related_fragments[rid].add_related_from(source_rid)
649
+
650
+ if connect_source_identity
651
+ related_relationship = related_klass._relationships[relationship.inverse_relationship]
652
+ if related_relationship
653
+ related_fragments[rid].add_related_identity(related_relationship.name, source_rid)
654
+ end
655
+ end
656
+
657
+ relation_position = relation_positions[row[2].underscore.pluralize]
658
+ model_fields = relation_position[:model_fields]
659
+ cache_field = relation_position[:cache_field]
660
+ cache_offset = relation_position[:cache_offset]
661
+ field_offset = relation_position[:field_offset]
662
+
663
+ if cache_field
664
+ related_fragments[rid].cache = cast_to_attribute_type(row[cache_offset], cache_field[:type])
665
+ end
666
+
667
+ if attributes.length > 0
668
+ model_fields.each_with_index do |k, idx|
669
+ related_fragments[rid].add_attribute(k[0], cast_to_attribute_type(row[idx + field_offset], k[1][:type]))
670
+ end
671
+ end
672
+
673
+ linkage_fields.each_with_index do |linkage_field_details, idx|
674
+ relationship = linkage_field_details[:relationship]
675
+ related_fragments[rid].initialize_related(relationship.name)
676
+ related_id = row[linkage_offset + idx]
677
+ if related_id
678
+ related_rid = JSONAPI::ResourceIdentity.new(linkage_field_details[:resource_klass], related_id)
679
+ related_fragments[rid].add_related_identity(relationship.name, related_rid)
680
+ end
681
+ end
682
+ end
683
+ end
684
+
685
+ related_fragments
686
+ end
687
+
688
+ def apply_request_settings_to_records(records:,
689
+ join_manager: ActiveRelation::JoinManager.new(resource_klass: self),
690
+ resource_klass: self,
691
+ filters: {},
692
+ primary_keys: nil,
693
+ sort_criteria: nil,
694
+ sort_primary: nil,
695
+ paginator: nil,
696
+ options: {})
697
+
698
+ options[:_relation_helper_options] = { join_manager: join_manager, sort_fields: [] }
699
+
700
+ records = resource_klass.apply_joins(records, join_manager, options)
701
+
702
+ if primary_keys
703
+ records = records.where(_primary_key => primary_keys)
704
+ end
705
+
706
+ unless filters.empty?
707
+ records = resource_klass.filter_records(records, filters, options)
708
+ end
709
+
710
+ if sort_primary
711
+ records = records.order(_primary_key => :asc)
712
+ else
713
+ order_options = resource_klass.construct_order_options(sort_criteria)
714
+ records = resource_klass.sort_records(records, order_options, options)
715
+ end
716
+
717
+ if paginator
718
+ records = resource_klass.apply_pagination(records, paginator, order_options)
719
+ end
720
+
721
+ records
722
+ end
723
+
724
+ def apply_joins(records, join_manager, options)
725
+ join_manager.join(records, options)
726
+ end
727
+
728
+ def apply_pagination(records, paginator, order_options)
729
+ records = paginator.apply(records, order_options) if paginator
730
+ records
731
+ end
732
+
733
+ def apply_sort(records, order_options, options)
734
+ if order_options.any?
735
+ order_options.each_pair do |field, direction|
736
+ records = apply_single_sort(records, field, direction, options)
737
+ end
738
+ end
739
+
740
+ records
741
+ end
742
+
743
+ def apply_single_sort(records, field, direction, options)
744
+ context = options[:context]
745
+
746
+ strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]
747
+
748
+ options[:_relation_helper_options] ||= {}
749
+ options[:_relation_helper_options][:sort_fields] ||= []
750
+
751
+ if strategy
752
+ records = call_method_or_proc(strategy, records, direction, context)
753
+ else
754
+ join_manager = options.dig(:_relation_helper_options, :join_manager)
755
+ sort_field = join_manager ? get_aliased_field(field, join_manager) : field
756
+ options[:_relation_helper_options][:sort_fields].push("#{sort_field}")
757
+ records = records.order(Arel.sql("#{sort_field} #{direction}"))
758
+ end
759
+ records
760
+ end
761
+
762
+ # Assumes ActiveRecord's counting. Override if you need a different counting method
763
+ def count_records(records)
764
+ if (Rails::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR >= 6
765
+ records.count(:all)
766
+ else
767
+ records.count
768
+ end
769
+ end
770
+
771
+ def filter_records(records, filters, options)
772
+ if _polymorphic
773
+ _polymorphic_resource_klasses.each do |klass|
774
+ records = klass.apply_filters(records, filters, options)
775
+ end
776
+ else
777
+ records = apply_filters(records, filters, options)
778
+ end
779
+ records
780
+ end
781
+
782
+ def construct_order_options(sort_params)
783
+ if _polymorphic
784
+ warn "Sorting is not supported on polymorphic relationships"
785
+ else
786
+ super(sort_params)
787
+ end
788
+ end
789
+
790
+ def sort_records(records, order_options, options)
791
+ apply_sort(records, order_options, options)
792
+ end
793
+
794
+ def concat_table_field(table, field, quoted = false)
795
+ if table.blank? || field.to_s.include?('.')
796
+ # :nocov:
797
+ if quoted
798
+ quote(field)
799
+ else
800
+ field.to_s
801
+ end
802
+ # :nocov:
803
+ else
804
+ if quoted
805
+ "#{quote(table)}.#{quote(field)}"
806
+ else
807
+ # :nocov:
808
+ "#{table.to_s}.#{field.to_s}"
809
+ # :nocov:
810
+ end
811
+ end
812
+ end
813
+
814
+ def sql_field_with_alias(table, field, quoted = true)
815
+ Arel.sql("#{concat_table_field(table, field, quoted)} AS #{alias_table_field(table, field, quoted)}")
816
+ end
817
+
818
+ def alias_table_field(table, field, quoted = false)
819
+ if table.blank? || field.to_s.include?('.')
820
+ # :nocov:
821
+ if quoted
822
+ quote(field)
823
+ else
824
+ field.to_s
825
+ end
826
+ # :nocov:
827
+ else
828
+ if quoted
829
+ # :nocov:
830
+ quote("#{table.to_s}_#{field.to_s}")
831
+ # :nocov:
832
+ else
833
+ "#{table.to_s}_#{field.to_s}"
834
+ end
835
+ end
836
+ end
837
+
838
+ def quote(field)
839
+ "\"#{field.to_s}\""
840
+ end
841
+
842
+
843
+ def apply_filters(records, filters, options = {})
844
+ if filters
845
+ filters.each do |filter, value|
846
+ records = apply_filter(records, filter, value, options)
847
+ end
848
+ end
849
+
850
+ records
851
+ end
852
+
853
+ def get_aliased_field(path_with_field, join_manager)
854
+ path = JSONAPI::Path.new(resource_klass: self, path_string: path_with_field)
855
+
856
+ relationship_segment = path.segments[-2]
857
+ field_segment = path.segments[-1]
858
+
859
+ if relationship_segment
860
+ join_details = join_manager.join_details[path.last_relationship]
861
+ table_alias = join_details[:alias]
862
+ else
863
+ table_alias = self._table_name
864
+ end
865
+
866
+ concat_table_field(table_alias, field_segment.delegated_field_name)
867
+ end
868
+
869
+ def apply_filter(records, filter, value, options = {})
870
+ strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
871
+
872
+ if strategy
873
+ records = call_method_or_proc(strategy, records, value, options)
874
+ else
875
+ join_manager = options.dig(:_relation_helper_options, :join_manager)
876
+ field = join_manager ? get_aliased_field(filter, join_manager) : filter
877
+ records = records.where(Arel.sql(field) => value)
878
+ end
879
+
880
+ records
881
+ end
882
+ end
883
+ end
884
+ end