jsonapi-resources 0.9.11 → 0.10.7

Sign up to get free protection for your applications and to get access to all the features.
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