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