jsonapi-resources 0.9.3 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +297 -0
  8. data/lib/jsonapi/active_relation_resource.rb +879 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -105
  10. data/lib/jsonapi/basic_resource.rb +1162 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +127 -0
  12. data/lib/jsonapi/compiled_json.rb +11 -1
  13. data/lib/jsonapi/configuration.rb +63 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +63 -40
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -65
  19. data/lib/jsonapi/link_builder.rb +74 -80
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +74 -16
  22. data/lib/jsonapi/path.rb +43 -0
  23. data/lib/jsonapi/path_segment.rb +76 -0
  24. data/lib/jsonapi/processor.rb +237 -110
  25. data/lib/jsonapi/relationship.rb +144 -15
  26. data/lib/jsonapi/request_parser.rb +412 -357
  27. data/lib/jsonapi/resource.rb +3 -1263
  28. data/lib/jsonapi/resource_controller_metal.rb +5 -2
  29. data/lib/jsonapi/resource_fragment.rb +47 -0
  30. data/lib/jsonapi/resource_id_tree.rb +112 -0
  31. data/lib/jsonapi/resource_identity.rb +42 -0
  32. data/lib/jsonapi/resource_serializer.rb +143 -285
  33. data/lib/jsonapi/resource_set.rb +176 -0
  34. data/lib/jsonapi/resources/railtie.rb +9 -0
  35. data/lib/jsonapi/resources/version.rb +1 -1
  36. data/lib/jsonapi/response_document.rb +105 -83
  37. data/lib/jsonapi/routing_ext.rb +48 -26
  38. data/lib/jsonapi-resources.rb +20 -4
  39. data/lib/tasks/check_upgrade.rake +52 -0
  40. metadata +47 -17
  41. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  42. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  43. data/lib/jsonapi/operation_results.rb +0 -35
  44. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -0,0 +1,879 @@
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
+ records
328
+ end
329
+
330
+ def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
331
+ records = relationship.parent_resource.records_for_source_to_related(options)
332
+ strategy = relationship.options[:apply_join]
333
+
334
+ if strategy
335
+ records = call_method_or_proc(strategy, records, relationship, resource_type, join_type, options)
336
+ else
337
+ records = apply_join(records: records,
338
+ relationship: relationship,
339
+ resource_type: resource_type,
340
+ join_type: join_type,
341
+ options: options)
342
+ end
343
+
344
+ records
345
+ end
346
+
347
+ def join_relationship(records:, relationship:, resource_type: nil, join_type: :inner, options: {})
348
+ relationship_records = relationship_records(relationship: relationship,
349
+ join_type: join_type,
350
+ resource_type: resource_type,
351
+ options: options)
352
+ records.merge(relationship_records)
353
+ end
354
+
355
+ protected
356
+
357
+ def to_one_relationships_for_linkage(include_related)
358
+ include_related ||= {}
359
+ relationships = []
360
+ _relationships.each do |name, relationship|
361
+ if relationship.is_a?(JSONAPI::Relationship::ToOne) && !include_related.has_key?(name) && relationship.include_optional_linkage_data?
362
+ relationships << name
363
+ end
364
+ end
365
+ relationships
366
+ end
367
+
368
+ def find_record_by_key(key, options = {})
369
+ record = apply_request_settings_to_records(records: records(options), primary_keys: key, options: options).first
370
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if record.nil?
371
+ record
372
+ end
373
+
374
+ def find_records_by_keys(keys, options = {})
375
+ apply_request_settings_to_records(records: records(options), primary_keys: keys, options: options)
376
+ end
377
+
378
+ def find_related_monomorphic_fragments(source_rids, relationship, options, connect_source_identity)
379
+ filters = options.fetch(:filters, {})
380
+ source_ids = source_rids.collect {|rid| rid.id}
381
+
382
+ include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
383
+ resource_klass = relationship.resource_klass
384
+ linkage_relationships = resource_klass.to_one_relationships_for_linkage(include_directives[:include_related])
385
+
386
+ sort_criteria = []
387
+ options[:sort_criteria].try(:each) do |sort|
388
+ field = sort[:field].to_s == 'id' ? resource_klass._primary_key : sort[:field]
389
+ sort_criteria << { field: field, direction: sort[:direction] }
390
+ end
391
+
392
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
393
+ source_relationship: relationship,
394
+ relationships: linkage_relationships,
395
+ sort_criteria: sort_criteria,
396
+ filters: filters)
397
+
398
+ paginator = options[:paginator]
399
+
400
+ records = apply_request_settings_to_records(records: records_for_source_to_related(options),
401
+ resource_klass: resource_klass,
402
+ sort_criteria: sort_criteria,
403
+ primary_keys: source_ids,
404
+ paginator: paginator,
405
+ filters: filters,
406
+ join_manager: join_manager,
407
+ options: options)
408
+
409
+ resource_table_alias = join_manager.join_details_by_relationship(relationship)[:alias]
410
+
411
+ pluck_fields = [
412
+ Arel.sql("#{_table_name}.#{_primary_key} AS \"source_id\""),
413
+ sql_field_with_alias(resource_table_alias, resource_klass._primary_key)
414
+ ]
415
+
416
+ cache_field = resource_klass.attribute_to_model_field(:_cache_field) if options[:cache]
417
+ if cache_field
418
+ pluck_fields << sql_field_with_alias(resource_table_alias, cache_field[:name])
419
+ end
420
+
421
+ linkage_fields = []
422
+
423
+ linkage_relationships.each do |name|
424
+ linkage_relationship = resource_klass._relationship(name)
425
+
426
+ if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
427
+ linkage_relationship.resource_types.each do |resource_type|
428
+ klass = resource_klass_for(resource_type)
429
+ linkage_fields << {relationship_name: name, resource_klass: klass}
430
+
431
+ linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
432
+ primary_key = klass._primary_key
433
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
434
+ end
435
+ else
436
+ klass = linkage_relationship.resource_klass
437
+ linkage_fields << {relationship_name: name, resource_klass: klass}
438
+
439
+ linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
440
+ primary_key = klass._primary_key
441
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
442
+ end
443
+ end
444
+
445
+ model_fields = {}
446
+ attributes = options[:attributes]
447
+ attributes.try(:each) do |attribute|
448
+ model_field = resource_klass.attribute_to_model_field(attribute)
449
+ model_fields[attribute] = model_field
450
+ pluck_fields << sql_field_with_alias(resource_table_alias, model_field[:name])
451
+ end
452
+
453
+ sort_fields = options.dig(:_relation_helper_options, :sort_fields)
454
+ sort_fields.try(:each) do |field|
455
+ pluck_fields << Arel.sql(field)
456
+ end
457
+
458
+ fragments = {}
459
+ rows = records.distinct.pluck(*pluck_fields)
460
+ rows.each do |row|
461
+ rid = JSONAPI::ResourceIdentity.new(resource_klass, row[1])
462
+
463
+ fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
464
+
465
+ attributes_offset = 2
466
+
467
+ if cache_field
468
+ fragments[rid].cache = cast_to_attribute_type(row[attributes_offset], cache_field[:type])
469
+ attributes_offset+= 1
470
+ end
471
+
472
+ model_fields.each_with_index do |k, idx|
473
+ fragments[rid].add_attribute(k[0], cast_to_attribute_type(row[idx + attributes_offset], k[1][:type]))
474
+ attributes_offset+= 1
475
+ end
476
+
477
+ source_rid = JSONAPI::ResourceIdentity.new(self, row[0])
478
+
479
+ fragments[rid].add_related_from(source_rid)
480
+
481
+ linkage_fields.each do |linkage_field|
482
+ fragments[rid].initialize_related(linkage_field[:relationship_name])
483
+ related_id = row[attributes_offset]
484
+ if related_id
485
+ related_rid = JSONAPI::ResourceIdentity.new(linkage_field[:resource_klass], related_id)
486
+ fragments[rid].add_related_identity(linkage_field[:relationship_name], related_rid)
487
+ end
488
+ attributes_offset+= 1
489
+ end
490
+
491
+ if connect_source_identity
492
+ related_relationship = resource_klass._relationships[relationship.inverse_relationship]
493
+ if related_relationship
494
+ fragments[rid].add_related_identity(related_relationship.name, source_rid)
495
+ end
496
+ end
497
+ end
498
+
499
+ fragments
500
+ end
501
+
502
+ # Gets resource identities where the related resource is polymorphic and the resource type and id
503
+ # are stored on the primary resources. Cache fields will always be on the related resources.
504
+ def find_related_polymorphic_fragments(source_rids, relationship, options, connect_source_identity)
505
+ filters = options.fetch(:filters, {})
506
+ source_ids = source_rids.collect {|rid| rid.id}
507
+
508
+ resource_klass = relationship.resource_klass
509
+ include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
510
+
511
+ linkage_relationships = []
512
+
513
+ resource_types = relationship.resource_types
514
+
515
+ resource_types.each do |resource_type|
516
+ related_resource_klass = resource_klass_for(resource_type)
517
+ relationships = related_resource_klass.to_one_relationships_for_linkage(include_directives[:include_related])
518
+ relationships.each do |r|
519
+ linkage_relationships << "##{resource_type}.#{r}"
520
+ end
521
+ end
522
+
523
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
524
+ source_relationship: relationship,
525
+ relationships: linkage_relationships,
526
+ filters: filters)
527
+
528
+ paginator = options[:paginator]
529
+
530
+ # Note: We will sort by the source table. Without using unions we can't sort on a polymorphic relationship
531
+ # in any manner that makes sense
532
+ records = apply_request_settings_to_records(records: records_for_source_to_related(options),
533
+ resource_klass: resource_klass,
534
+ sort_primary: true,
535
+ primary_keys: source_ids,
536
+ paginator: paginator,
537
+ filters: filters,
538
+ join_manager: join_manager,
539
+ options: options)
540
+
541
+ primary_key = concat_table_field(_table_name, _primary_key)
542
+ related_key = concat_table_field(_table_name, relationship.foreign_key)
543
+ related_type = concat_table_field(_table_name, relationship.polymorphic_type)
544
+
545
+ pluck_fields = [
546
+ Arel.sql("#{primary_key} AS #{alias_table_field(_table_name, _primary_key)}"),
547
+ Arel.sql("#{related_key} AS #{alias_table_field(_table_name, relationship.foreign_key)}"),
548
+ Arel.sql("#{related_type} AS #{alias_table_field(_table_name, relationship.polymorphic_type)}")
549
+ ]
550
+
551
+ # Get the additional fields from each relation. There's a limitation that the fields must exist in each relation
552
+
553
+ relation_positions = {}
554
+ relation_index = pluck_fields.length
555
+
556
+ attributes = options.fetch(:attributes, [])
557
+
558
+ # Add resource specific fields
559
+ if resource_types.nil? || resource_types.length == 0
560
+ # :nocov:
561
+ warn "No resource types found for polymorphic relationship."
562
+ # :nocov:
563
+ else
564
+ resource_types.try(:each) do |type|
565
+ related_klass = resource_klass_for(type.to_s)
566
+
567
+ cache_field = related_klass.attribute_to_model_field(:_cache_field) if options[:cache]
568
+
569
+ table_alias = join_manager.source_join_details(type)[:alias]
570
+
571
+ cache_offset = relation_index
572
+ if cache_field
573
+ pluck_fields << sql_field_with_alias(table_alias, cache_field[:name])
574
+ relation_index+= 1
575
+ end
576
+
577
+ model_fields = {}
578
+ field_offset = relation_index
579
+ attributes.try(:each) do |attribute|
580
+ model_field = related_klass.attribute_to_model_field(attribute)
581
+ model_fields[attribute] = model_field
582
+ pluck_fields << sql_field_with_alias(table_alias, model_field[:name])
583
+ relation_index+= 1
584
+ end
585
+
586
+ model_offset = relation_index
587
+ model_fields.each do |_k, v|
588
+ pluck_fields << Arel.sql("#{concat_table_field(table_alias, v[:name])}")
589
+ relation_index+= 1
590
+ end
591
+
592
+ relation_positions[type] = {relation_klass: related_klass,
593
+ cache_field: cache_field,
594
+ cache_offset: cache_offset,
595
+ model_fields: model_fields,
596
+ model_offset: model_offset,
597
+ field_offset: field_offset}
598
+ end
599
+ end
600
+
601
+ # Add to_one linkage fields
602
+ linkage_fields = []
603
+ linkage_offset = relation_index
604
+
605
+ linkage_relationships.each do |linkage_relationship_path|
606
+ path = JSONAPI::Path.new(resource_klass: self,
607
+ path_string: "#{relationship.name}#{linkage_relationship_path}",
608
+ ensure_default_field: false)
609
+
610
+ linkage_relationship = path.segments[-1].relationship
611
+
612
+ if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
613
+ linkage_relationship.resource_types.each do |resource_type|
614
+ klass = resource_klass_for(resource_type)
615
+ linkage_fields << {relationship: linkage_relationship, resource_klass: klass}
616
+
617
+ linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
618
+ primary_key = klass._primary_key
619
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
620
+ end
621
+ else
622
+ klass = linkage_relationship.resource_klass
623
+ linkage_fields << {relationship: linkage_relationship, resource_klass: klass}
624
+
625
+ linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
626
+ primary_key = klass._primary_key
627
+ pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
628
+ end
629
+ end
630
+
631
+ rows = records.distinct.pluck(*pluck_fields)
632
+
633
+ related_fragments = {}
634
+
635
+ rows.each do |row|
636
+ unless row[1].nil? || row[2].nil?
637
+ related_klass = resource_klass_for(row[2])
638
+
639
+ rid = JSONAPI::ResourceIdentity.new(related_klass, row[1])
640
+ related_fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
641
+
642
+ source_rid = JSONAPI::ResourceIdentity.new(self, row[0])
643
+ related_fragments[rid].add_related_from(source_rid)
644
+
645
+ if connect_source_identity
646
+ related_relationship = related_klass._relationships[relationship.inverse_relationship]
647
+ if related_relationship
648
+ related_fragments[rid].add_related_identity(related_relationship.name, source_rid)
649
+ end
650
+ end
651
+
652
+ relation_position = relation_positions[row[2].underscore.pluralize]
653
+ model_fields = relation_position[:model_fields]
654
+ cache_field = relation_position[:cache_field]
655
+ cache_offset = relation_position[:cache_offset]
656
+ field_offset = relation_position[:field_offset]
657
+
658
+ if cache_field
659
+ related_fragments[rid].cache = cast_to_attribute_type(row[cache_offset], cache_field[:type])
660
+ end
661
+
662
+ if attributes.length > 0
663
+ model_fields.each_with_index do |k, idx|
664
+ related_fragments[rid].add_attribute(k[0], cast_to_attribute_type(row[idx + field_offset], k[1][:type]))
665
+ end
666
+ end
667
+
668
+ linkage_fields.each_with_index do |linkage_field_details, idx|
669
+ relationship = linkage_field_details[:relationship]
670
+ related_fragments[rid].initialize_related(relationship.name)
671
+ related_id = row[linkage_offset + idx]
672
+ if related_id
673
+ related_rid = JSONAPI::ResourceIdentity.new(linkage_field_details[:resource_klass], related_id)
674
+ related_fragments[rid].add_related_identity(relationship.name, related_rid)
675
+ end
676
+ end
677
+ end
678
+ end
679
+
680
+ related_fragments
681
+ end
682
+
683
+ def apply_request_settings_to_records(records:,
684
+ join_manager: ActiveRelation::JoinManager.new(resource_klass: self),
685
+ resource_klass: self,
686
+ filters: {},
687
+ primary_keys: nil,
688
+ sort_criteria: nil,
689
+ sort_primary: nil,
690
+ paginator: nil,
691
+ options: {})
692
+
693
+ options[:_relation_helper_options] = { join_manager: join_manager, sort_fields: [] }
694
+
695
+ records = resource_klass.apply_joins(records, join_manager, options)
696
+
697
+ if primary_keys
698
+ records = records.where(_primary_key => primary_keys)
699
+ end
700
+
701
+ unless filters.empty?
702
+ records = resource_klass.filter_records(records, filters, options)
703
+ end
704
+
705
+ if sort_primary
706
+ records = records.order(_primary_key => :asc)
707
+ else
708
+ order_options = resource_klass.construct_order_options(sort_criteria)
709
+ records = resource_klass.sort_records(records, order_options, options)
710
+ end
711
+
712
+ if paginator
713
+ records = resource_klass.apply_pagination(records, paginator, order_options)
714
+ end
715
+
716
+ records
717
+ end
718
+
719
+ def apply_joins(records, join_manager, options)
720
+ join_manager.join(records, options)
721
+ end
722
+
723
+ def apply_pagination(records, paginator, order_options)
724
+ records = paginator.apply(records, order_options) if paginator
725
+ records
726
+ end
727
+
728
+ def apply_sort(records, order_options, options)
729
+ if order_options.any?
730
+ order_options.each_pair do |field, direction|
731
+ records = apply_single_sort(records, field, direction, options)
732
+ end
733
+ end
734
+
735
+ records
736
+ end
737
+
738
+ def apply_single_sort(records, field, direction, options)
739
+ context = options[:context]
740
+
741
+ strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]
742
+
743
+ options[:_relation_helper_options] ||= {}
744
+ options[:_relation_helper_options][:sort_fields] ||= []
745
+
746
+ if strategy
747
+ records = call_method_or_proc(strategy, records, direction, context)
748
+ else
749
+ join_manager = options.dig(:_relation_helper_options, :join_manager)
750
+ sort_field = join_manager ? get_aliased_field(field, join_manager) : field
751
+ options[:_relation_helper_options][:sort_fields].push("#{sort_field}")
752
+ records = records.order(Arel.sql("#{sort_field} #{direction}"))
753
+ end
754
+ records
755
+ end
756
+
757
+ # Assumes ActiveRecord's counting. Override if you need a different counting method
758
+ def count_records(records)
759
+ if (Rails::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR >= 6
760
+ records.count(:all)
761
+ else
762
+ records.count
763
+ end
764
+ end
765
+
766
+ def filter_records(records, filters, options)
767
+ if _polymorphic
768
+ _polymorphic_resource_klasses.each do |klass|
769
+ records = klass.apply_filters(records, filters, options)
770
+ end
771
+ else
772
+ records = apply_filters(records, filters, options)
773
+ end
774
+ records
775
+ end
776
+
777
+ def construct_order_options(sort_params)
778
+ if _polymorphic
779
+ warn "Sorting is not supported on polymorphic relationships"
780
+ else
781
+ super(sort_params)
782
+ end
783
+ end
784
+
785
+ def sort_records(records, order_options, options)
786
+ apply_sort(records, order_options, options)
787
+ end
788
+
789
+ def concat_table_field(table, field, quoted = false)
790
+ if table.blank? || field.to_s.include?('.')
791
+ # :nocov:
792
+ if quoted
793
+ quote(field)
794
+ else
795
+ field.to_s
796
+ end
797
+ # :nocov:
798
+ else
799
+ if quoted
800
+ "#{quote(table)}.#{quote(field)}"
801
+ else
802
+ # :nocov:
803
+ "#{table.to_s}.#{field.to_s}"
804
+ # :nocov:
805
+ end
806
+ end
807
+ end
808
+
809
+ def sql_field_with_alias(table, field, quoted = true)
810
+ Arel.sql("#{concat_table_field(table, field, quoted)} AS #{alias_table_field(table, field, quoted)}")
811
+ end
812
+
813
+ def alias_table_field(table, field, quoted = false)
814
+ if table.blank? || field.to_s.include?('.')
815
+ # :nocov:
816
+ if quoted
817
+ quote(field)
818
+ else
819
+ field.to_s
820
+ end
821
+ # :nocov:
822
+ else
823
+ if quoted
824
+ # :nocov:
825
+ quote("#{table.to_s}_#{field.to_s}")
826
+ # :nocov:
827
+ else
828
+ "#{table.to_s}_#{field.to_s}"
829
+ end
830
+ end
831
+ end
832
+
833
+ def quote(field)
834
+ "\"#{field.to_s}\""
835
+ end
836
+
837
+
838
+ def apply_filters(records, filters, options = {})
839
+ if filters
840
+ filters.each do |filter, value|
841
+ records = apply_filter(records, filter, value, options)
842
+ end
843
+ end
844
+
845
+ records
846
+ end
847
+
848
+ def get_aliased_field(path_with_field, join_manager)
849
+ path = JSONAPI::Path.new(resource_klass: self, path_string: path_with_field)
850
+
851
+ relationship_segment = path.segments[-2]
852
+ field_segment = path.segments[-1]
853
+
854
+ if relationship_segment
855
+ join_details = join_manager.join_details[path.last_relationship]
856
+ table_alias = join_details[:alias]
857
+ else
858
+ table_alias = self._table_name
859
+ end
860
+
861
+ concat_table_field(table_alias, field_segment.delegated_field_name)
862
+ end
863
+
864
+ def apply_filter(records, filter, value, options = {})
865
+ strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
866
+
867
+ if strategy
868
+ records = call_method_or_proc(strategy, records, value, options)
869
+ else
870
+ join_manager = options.dig(:_relation_helper_options, :join_manager)
871
+ field = join_manager ? get_aliased_field(filter, join_manager) : filter
872
+ records = records.where(Arel.sql(field) => value)
873
+ end
874
+
875
+ records
876
+ end
877
+ end
878
+ end
879
+ end