jsonapi-resources 0.9.12 → 0.10.0

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