jsonapi-resources 0.10.6 → 0.11.0.beta2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +39 -2
  4. data/lib/generators/jsonapi/controller_generator.rb +2 -0
  5. data/lib/generators/jsonapi/resource_generator.rb +2 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +3 -2
  7. data/lib/jsonapi/active_relation/join_manager.rb +30 -18
  8. data/lib/jsonapi/active_relation/join_manager_v10.rb +305 -0
  9. data/lib/jsonapi/active_relation_retrieval.rb +885 -0
  10. data/lib/jsonapi/active_relation_retrieval_v09.rb +715 -0
  11. data/lib/jsonapi/{active_relation_resource.rb → active_relation_retrieval_v10.rb} +113 -135
  12. data/lib/jsonapi/acts_as_resource_controller.rb +49 -49
  13. data/lib/jsonapi/cached_response_fragment.rb +4 -2
  14. data/lib/jsonapi/callbacks.rb +2 -0
  15. data/lib/jsonapi/compiled_json.rb +2 -0
  16. data/lib/jsonapi/configuration.rb +35 -15
  17. data/lib/jsonapi/error.rb +2 -0
  18. data/lib/jsonapi/error_codes.rb +2 -0
  19. data/lib/jsonapi/exceptions.rb +2 -0
  20. data/lib/jsonapi/formatter.rb +2 -0
  21. data/lib/jsonapi/include_directives.rb +77 -19
  22. data/lib/jsonapi/link_builder.rb +2 -0
  23. data/lib/jsonapi/mime_types.rb +6 -10
  24. data/lib/jsonapi/naive_cache.rb +2 -0
  25. data/lib/jsonapi/operation.rb +2 -0
  26. data/lib/jsonapi/operation_result.rb +2 -0
  27. data/lib/jsonapi/paginator.rb +2 -0
  28. data/lib/jsonapi/path.rb +2 -0
  29. data/lib/jsonapi/path_segment.rb +4 -2
  30. data/lib/jsonapi/processor.rb +95 -140
  31. data/lib/jsonapi/relationship.rb +89 -35
  32. data/lib/jsonapi/{request_parser.rb → request.rb} +157 -164
  33. data/lib/jsonapi/resource.rb +7 -2
  34. data/lib/jsonapi/{basic_resource.rb → resource_common.rb} +187 -88
  35. data/lib/jsonapi/resource_controller.rb +2 -0
  36. data/lib/jsonapi/resource_controller_metal.rb +2 -0
  37. data/lib/jsonapi/resource_fragment.rb +17 -15
  38. data/lib/jsonapi/resource_identity.rb +6 -0
  39. data/lib/jsonapi/resource_serializer.rb +20 -4
  40. data/lib/jsonapi/resource_set.rb +36 -16
  41. data/lib/jsonapi/resource_tree.rb +191 -0
  42. data/lib/jsonapi/resources/railtie.rb +3 -1
  43. data/lib/jsonapi/resources/version.rb +3 -1
  44. data/lib/jsonapi/response_document.rb +4 -2
  45. data/lib/jsonapi/routing_ext.rb +4 -2
  46. data/lib/jsonapi/simple_resource.rb +13 -0
  47. data/lib/jsonapi-resources.rb +10 -4
  48. data/lib/tasks/check_upgrade.rake +3 -1
  49. metadata +47 -15
  50. data/lib/jsonapi/resource_id_tree.rb +0 -112
@@ -0,0 +1,715 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module ActiveRelationRetrievalV09
5
+ def find_related_ids(relationship, options = {})
6
+ self.class.find_related_fragments(self.fragment, relationship, options).keys.collect { |rid| rid.id }
7
+ end
8
+
9
+ # Override this on a resource to customize how the associated records
10
+ # are fetched for a model. Particularly helpful for authorization.
11
+ def records_for(relation_name)
12
+ _model.public_send relation_name
13
+ end
14
+
15
+ module ClassMethods
16
+ # Finds Resources using the `filters`. Pagination and sort options are used when provided
17
+ #
18
+ # @param filters [Hash] the filters hash
19
+ # @option options [Hash] :context The context of the request, set in the controller
20
+ # @option options [Hash] :sort_criteria The `sort criteria`
21
+ # @option options [Hash] :include_directives The `include_directives`
22
+ #
23
+ # @return [Array<Resource>] the Resource instances matching the filters, sorting and pagination rules.
24
+ def find(filters, options = {})
25
+ context = options[:context]
26
+
27
+ records = filter_records(records(options), filters, options)
28
+
29
+ sort_criteria = options.fetch(:sort_criteria) { [] }
30
+ order_options = construct_order_options(sort_criteria)
31
+ records = sort_records(records, order_options, context)
32
+
33
+ records = apply_pagination(records, options[:paginator], order_options)
34
+
35
+ resources_for(records, 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
+ count_records(filter_records(records(options), filters, options))
46
+ end
47
+
48
+ # Returns the single Resource identified by `key`
49
+ #
50
+ # @param key the primary key of the resource to find
51
+ # @option options [Hash] :context The context of the request, set in the controller
52
+ def find_by_key(key, options = {})
53
+ context = options[:context]
54
+ records = records(options)
55
+
56
+ records = apply_includes(records, options)
57
+ model = records.where({_primary_key => key}).first
58
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
59
+ self.resource_klass_for_model(model).new(model, context)
60
+ end
61
+
62
+ # Returns an array of Resources identified by the `keys` array
63
+ #
64
+ # @param keys [Array<key>] Array of primary keys to find resources for
65
+ # @option options [Hash] :context The context of the request, set in the controller
66
+ def find_by_keys(keys, options = {})
67
+ context = options[:context]
68
+ records = records(options)
69
+ records = apply_includes(records, options)
70
+ models = records.where({_primary_key => keys})
71
+ models.collect do |model|
72
+ self.resource_klass_for_model(model).new(model, context)
73
+ end
74
+ end
75
+
76
+ # Returns an array of Resources identified by the `keys` array. The resources are not filtered as this
77
+ # will have been done in a prior step
78
+ #
79
+ # @param keys [Array<key>] Array of primary keys to find resources for
80
+ # @option options [Hash] :context The context of the request, set in the controller
81
+ def find_to_populate_by_keys(keys, options = {})
82
+ records = records_for_populate(options).where(_primary_key => keys)
83
+ resources_for(records, options[:context])
84
+ end
85
+
86
+ # Finds Resource fragments using the `filters`. Pagination and sort options are used when provided.
87
+ # Note: This is incompatible with Polymorphic resources (which are going to come from two separate tables)
88
+ #
89
+ # @param filters [Hash] the filters hash
90
+ # @option options [Hash] :context The context of the request, set in the controller
91
+ # @option options [Hash] :sort_criteria The `sort criteria`
92
+ # @option options [Hash] :include_directives The `include_directives`
93
+ # @option options [Boolean] :cache Return the resources' cache field
94
+ #
95
+ # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field}]
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
+ context = options[:context]
100
+
101
+ sort_criteria = options.fetch(:sort_criteria) { [] }
102
+ order_options = construct_order_options(sort_criteria)
103
+
104
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
105
+ filters: filters,
106
+ sort_criteria: sort_criteria)
107
+
108
+ options[:_relation_helper_options] = { join_manager: join_manager, sort_fields: [] }
109
+ include_directives = options[:include_directives]
110
+
111
+ records = records(options)
112
+
113
+ records = apply_joins(records, join_manager, options)
114
+
115
+ records = filter_records(records, filters, options)
116
+
117
+ records = sort_records(records, order_options, context)
118
+
119
+ records = apply_pagination(records, options[:paginator], order_options)
120
+
121
+ resources = resources_for(records, context)
122
+
123
+ fragments = {}
124
+
125
+ linkage_relationships = to_one_relationships_for_linkage(include_directives.try(:[], :include_related))
126
+
127
+ resources.each do |resource|
128
+ rid = resource.identity
129
+
130
+ cache = options[:cache] ? resource.cache_field_value : nil
131
+
132
+ fragment = JSONAPI::ResourceFragment.new(rid, resource: resource, cache: cache, primary: true)
133
+ complete_linkages(fragment, linkage_relationships)
134
+ fragments[rid] ||= fragment
135
+ end
136
+
137
+ fragments
138
+ end
139
+
140
+ # Finds Resource Fragments related to the source resources through the specified relationship
141
+ #
142
+ # @param source_fragment [ResourceFragment>] The resource to find related ResourcesFragments for
143
+ # @param relationship_name [String | Symbol] The name of the relationship
144
+ # @option options [Hash] :context The context of the request, set in the controller
145
+ # @option options [Boolean] :cache Return the resources' cache field
146
+ #
147
+ # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, related: {relationship_name: [] }}}]
148
+ # the ResourceInstances matching the filters, sorting, and pagination rules along with any request
149
+ # additional_field values
150
+ def find_related_fragments(source_fragment, relationship, options)
151
+ fragments = {}
152
+ include_directives = options[:include_directives]
153
+
154
+ resource_klass = relationship.resource_klass
155
+
156
+ linkage_relationships = resource_klass.to_one_relationships_for_linkage(include_directives.try(:[], :include_related))
157
+
158
+ resources = source_fragment.resource.send(relationship.name, options)
159
+ resources = [] if resources.nil?
160
+ resources = [resources] unless resources.is_a?(Array)
161
+
162
+ # Do not pass in source as it will setup linkage data to the source
163
+ load_resources_to_fragments(fragments, resources, nil, relationship, linkage_relationships, options)
164
+
165
+ fragments
166
+ end
167
+
168
+ def find_included_fragments(source_fragments, relationship, options)
169
+ fragments = {}
170
+ include_directives = options[:include_directives]
171
+ resource_klass = relationship.resource_klass
172
+
173
+ linkage_relationships = if relationship.polymorphic?
174
+ []
175
+ else
176
+ resource_klass.to_one_relationships_for_linkage(include_directives.try(:[], :include_related))
177
+ end
178
+
179
+ source_fragments.each do |source_fragment|
180
+ raise "Missing resource in fragment #{__callee__}" unless source_fragment.resource.present?
181
+
182
+ resources = source_fragment.resource.send(relationship.name, options.except(:sort_criteria))
183
+ resources = [] if resources.nil?
184
+ resources = [resources] unless resources.is_a?(Array)
185
+
186
+ load_resources_to_fragments(fragments, resources, source_fragment, relationship, linkage_relationships, options)
187
+ end
188
+
189
+ fragments
190
+ end
191
+
192
+ def find_related_fragments_from_inverse(source, source_relationship, options, connect_source_identity)
193
+ raise "Not Implemented #{__callee__}"
194
+ end
195
+
196
+ # Counts Resources related to the source resource through the specified relationship
197
+ #
198
+ # @param source_rid [ResourceIdentity] Source resource identifier
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
+ #
202
+ # @return [Integer] the count
203
+
204
+ def count_related(source, relationship, options = {})
205
+ opts = options.except(:paginator)
206
+
207
+ related_resource_records = source.public_send("records_for_#{relationship.name}",
208
+ opts)
209
+ count_records(related_resource_records)
210
+ end
211
+
212
+ # This resource class (ActiveRelationResource) uses an `ActiveRecord::Relation` as the starting point for
213
+ # retrieving models. From this relation filters, sorts and joins are applied as needed.
214
+ # Depending on which phase of the request processing different `records` methods will be called, giving the user
215
+ # the opportunity to override them differently for performance and security reasons.
216
+
217
+ # begin `records`methods
218
+
219
+ # Base for the `records` methods that follow and is not directly used for accessing model data by this class.
220
+ # Overriding this method gives a single place to affect the `ActiveRecord::Relation` used for the resource.
221
+ #
222
+ # @option options [Hash] :context The context of the request, set in the controller
223
+ #
224
+ # @return [ActiveRecord::Relation]
225
+ def records_base(_options = {})
226
+ _model_class.all
227
+ end
228
+
229
+ # The `ActiveRecord::Relation` used for finding user requested models. This may be overridden to enforce
230
+ # permissions checks on the request.
231
+ #
232
+ # @option options [Hash] :context The context of the request, set in the controller
233
+ #
234
+ # @return [ActiveRecord::Relation]
235
+ def records(options = {})
236
+ records_base(options)
237
+ end
238
+
239
+ # The `ActiveRecord::Relation` used for populating the ResourceSet. Only resources that have been previously
240
+ # identified through the `records` method will be accessed. Thus it should not be necessary to reapply permissions
241
+ # checks. However if the model needs to include other models adding `includes` is appropriate
242
+ #
243
+ # @option options [Hash] :context The context of the request, set in the controller
244
+ #
245
+ # @return [ActiveRecord::Relation]
246
+ def records_for_populate(options = {})
247
+ records_base(options)
248
+ end
249
+
250
+ # The `ActiveRecord::Relation` used for the finding related resources.
251
+ #
252
+ # @option options [Hash] :context The context of the request, set in the controller
253
+ #
254
+ # @return [ActiveRecord::Relation]
255
+ def records_for_source_to_related(options = {})
256
+ records_base(options)
257
+ end
258
+
259
+ # end `records` methods
260
+
261
+ def load_resources_to_fragments(fragments, related_resources, source_resource, source_relationship, linkage_relationships, options)
262
+ cached = options[:cache]
263
+ primary = source_resource.nil?
264
+
265
+ related_resources.each do |related_resource|
266
+ cache = cached ? related_resource.cache_field_value : nil
267
+
268
+ fragment = fragments[related_resource.identity]
269
+
270
+ if fragment.nil?
271
+ fragment = JSONAPI::ResourceFragment.new(related_resource.identity,
272
+ resource: related_resource,
273
+ cache: cache,
274
+ primary: primary)
275
+
276
+ fragments[related_resource.identity] = fragment
277
+ complete_linkages(fragment, linkage_relationships)
278
+ end
279
+
280
+ if source_resource
281
+ source_resource.add_related_identity(source_relationship.name, related_resource.identity)
282
+ fragment.add_related_from(source_resource.identity)
283
+ fragment.add_related_identity(source_relationship.inverse_relationship, source_resource.identity)
284
+ end
285
+ end
286
+ end
287
+
288
+ def complete_linkages(fragment, linkage_relationships)
289
+ linkage_relationships.each do |linkage_relationship|
290
+ related_id = fragment.resource._model.attributes[linkage_relationship.foreign_key.to_s]
291
+
292
+ related_rid = if related_id
293
+ if linkage_relationship.polymorphic?
294
+ related_type = fragment.resource._model.attributes[linkage_relationship.polymorphic_type]
295
+ JSONAPI::ResourceIdentity.new(Resource.resource_klass_for(related_type), related_id)
296
+ else
297
+ klass = linkage_relationship.resource_klass
298
+ JSONAPI::ResourceIdentity.new(klass, related_id)
299
+ end
300
+ else
301
+ nil
302
+ end
303
+
304
+ fragment.add_related_identity(linkage_relationship.name, related_rid)
305
+ end
306
+ end
307
+
308
+ def apply_join(records:, relationship:, resource_type:, join_type:, options:)
309
+ if relationship.polymorphic? && relationship.belongs_to?
310
+ case join_type
311
+ when :inner
312
+ records = records.joins(resource_type.to_s.singularize.to_sym)
313
+ when :left
314
+ records = records.joins_left(resource_type.to_s.singularize.to_sym)
315
+ end
316
+ else
317
+ relation_name = relationship.relation_name(options)
318
+
319
+ # if relationship.alias_on_join
320
+ # alias_name = "#{relationship.preferred_alias}_#{relation_name}"
321
+ # case join_type
322
+ # when :inner
323
+ # records = records.joins_with_alias(relation_name, alias_name)
324
+ # when :left
325
+ # records = records.left_joins_with_alias(relation_name, alias_name)
326
+ # end
327
+ # else
328
+ case join_type
329
+ when :inner
330
+ records = records.joins(relation_name)
331
+ when :left
332
+ records = records.left_joins(relation_name)
333
+ end
334
+ end
335
+ # end
336
+ records
337
+ end
338
+
339
+ def define_relationship_methods(relationship_name, relationship_klass, options)
340
+ foreign_key = super
341
+
342
+ relationship = _relationship(relationship_name)
343
+
344
+ case relationship
345
+ when JSONAPI::Relationship::ToOne
346
+ associated = define_resource_relationship_accessor(:one, relationship_name)
347
+ args = [relationship, foreign_key, associated, relationship_name]
348
+
349
+ relationship.belongs_to? ? build_belongs_to(*args) : build_has_one(*args)
350
+ when JSONAPI::Relationship::ToMany
351
+ associated = define_resource_relationship_accessor(:many, relationship_name)
352
+
353
+ build_to_many(relationship, foreign_key, associated, relationship_name)
354
+ end
355
+ end
356
+
357
+
358
+ def define_resource_relationship_accessor(type, relationship_name)
359
+ associated_records_method_name = {
360
+ one: "record_for_#{relationship_name}",
361
+ many: "records_for_#{relationship_name}"
362
+ }.fetch(type)
363
+
364
+ define_on_resource associated_records_method_name do |options = {}|
365
+ relationship = self.class._relationships[relationship_name]
366
+ relation_name = relationship.relation_name(context: @context)
367
+ records = records_for(relation_name)
368
+
369
+ resource_klass = relationship.resource_klass
370
+
371
+ include_directives = options[:include_directives]&.include_directives&.dig(relationship_name)
372
+
373
+ options = options.dup
374
+ options[:include_directives] = include_directives
375
+
376
+ records = resource_klass.apply_includes(records, options)
377
+
378
+ filters = options.fetch(:filters, {})
379
+ unless filters.nil? || filters.empty?
380
+ records = resource_klass.apply_filters(records, filters, options)
381
+ end
382
+
383
+ sort_criteria = options.fetch(:sort_criteria, {})
384
+ order_options = relationship.resource_klass.construct_order_options(sort_criteria)
385
+ records = resource_klass.apply_sort(records, order_options, options)
386
+
387
+ paginator = options[:paginator]
388
+ if paginator
389
+ records = resource_klass.apply_pagination(records, paginator, order_options)
390
+ end
391
+
392
+ records
393
+ end
394
+
395
+ associated_records_method_name
396
+ end
397
+
398
+ def build_belongs_to(relationship, foreign_key, associated_records_method_name, relationship_name)
399
+ # Calls method matching foreign key name on model instance
400
+ define_on_resource foreign_key do
401
+ @model.method(foreign_key).call
402
+ end
403
+
404
+ # Returns instantiated related resource object or nil
405
+ define_on_resource relationship_name do |options = {}|
406
+ relationship = self.class._relationships[relationship_name]
407
+
408
+ if relationship.polymorphic?
409
+ associated_model = public_send(associated_records_method_name)
410
+ resource_klass = self.class.resource_klass_for_model(associated_model) if associated_model
411
+ return resource_klass.new(associated_model, @context) if resource_klass
412
+ else
413
+ resource_klass = relationship.resource_klass
414
+ if resource_klass
415
+ associated_model = public_send(associated_records_method_name)
416
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ def build_has_one(relationship, foreign_key, associated_records_method_name, relationship_name)
423
+ # Returns primary key name of related resource class
424
+ define_on_resource foreign_key do
425
+ relationship = self.class._relationships[relationship_name]
426
+
427
+ record = public_send(associated_records_method_name)
428
+ return nil if record.nil?
429
+ record.public_send(relationship.resource_klass._primary_key)
430
+ end
431
+
432
+ # Returns instantiated related resource object or nil
433
+ define_on_resource relationship_name do |options = {}|
434
+ relationship = self.class._relationships[relationship_name]
435
+
436
+ if relationship.polymorphic?
437
+ associated_model = public_send(associated_records_method_name)
438
+ resource_klass = self.class.resource_klass_for_model(associated_model) if associated_model
439
+ return resource_klass.new(associated_model, @context) if resource_klass && associated_model
440
+ else
441
+ resource_klass = relationship.resource_klass
442
+ if resource_klass
443
+ associated_model = public_send(associated_records_method_name)
444
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ def build_to_many(relationship, foreign_key, associated_records_method_name, relationship_name)
451
+ # Returns array of primary keys of related resource classes
452
+ define_on_resource foreign_key do
453
+ records = public_send(associated_records_method_name)
454
+ return records.collect do |record|
455
+ record.public_send(relationship.resource_klass._primary_key)
456
+ end
457
+ end
458
+
459
+ # Returns array of instantiated related resource objects
460
+ define_on_resource relationship_name do |options = {}|
461
+ relationship = self.class._relationships[relationship_name]
462
+
463
+ resource_klass = relationship.resource_klass
464
+ records = public_send(associated_records_method_name, options)
465
+
466
+ return records.collect do |record|
467
+ if relationship.polymorphic?
468
+ resource_klass = self.class.resource_for_model(record)
469
+ end
470
+ resource_klass.new(record, @context)
471
+ end
472
+ end
473
+ end
474
+
475
+ def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
476
+ case model_includes
477
+ when Array
478
+ return model_includes.map do |value|
479
+ resolve_relationship_names_to_relations(resource_klass, value, options)
480
+ end
481
+ when Hash
482
+ model_includes.keys.each do |key|
483
+ relationship = resource_klass._relationships[key]
484
+ value = model_includes[key]
485
+ model_includes.delete(key)
486
+ model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
487
+ end
488
+ return model_includes
489
+ when Symbol
490
+ relationship = resource_klass._relationships[model_includes]
491
+ return relationship.relation_name(options)
492
+ end
493
+ end
494
+
495
+ def apply_includes(records, options = {})
496
+ include_directives = options[:include_directives]
497
+ if include_directives
498
+ model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
499
+ records = records.includes(model_includes)
500
+ end
501
+
502
+ records
503
+ end
504
+
505
+ def apply_joins(records, join_manager, options)
506
+ join_manager.join(records, options)
507
+ end
508
+
509
+ def apply_pagination(records, paginator, order_options)
510
+ records = paginator.apply(records, order_options) if paginator
511
+ records
512
+ end
513
+
514
+ def apply_sort(records, order_options, options)
515
+ if order_options.any?
516
+ order_options.each_pair do |field, direction|
517
+ records = apply_single_sort(records, field, direction, options)
518
+ end
519
+ end
520
+
521
+ records
522
+ end
523
+
524
+ def apply_single_sort(records, field, direction, options)
525
+ strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]
526
+
527
+ delegated_field = attribute_to_model_field(field)
528
+
529
+ options[:_relation_helper_options] ||= {}
530
+ options[:_relation_helper_options][:sort_fields] ||= []
531
+
532
+ if strategy
533
+ records = call_method_or_proc(strategy, records, direction, options)
534
+ else
535
+ join_manager = options.dig(:_relation_helper_options, :join_manager)
536
+ sort_field = join_manager ? get_aliased_field(delegated_field[:name], join_manager) : delegated_field[:name]
537
+ options[:_relation_helper_options][:sort_fields].push("#{sort_field}")
538
+ records = records.order(Arel.sql("#{sort_field} #{direction}"))
539
+ end
540
+ records
541
+ end
542
+
543
+ def _lookup_association_chain(model_names)
544
+ associations = []
545
+ model_names.inject do |prev, current|
546
+ association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
547
+ assoc.name.to_s.underscore == current.underscore
548
+ end
549
+ associations << association
550
+ association.class_name
551
+ end
552
+
553
+ associations
554
+ end
555
+
556
+ def _build_joins(associations)
557
+ joins = []
558
+
559
+ associations.inject do |prev, current|
560
+ joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
561
+ current
562
+ end
563
+ joins.join("\n")
564
+ end
565
+
566
+ def concat_table_field(table, field, quoted = false)
567
+ if table.blank?
568
+ split_table, split_field = field.to_s.split('.')
569
+ if split_table && split_field
570
+ table = split_table
571
+ field = split_field
572
+ end
573
+ end
574
+ if table.blank?
575
+ # :nocov:
576
+ if quoted
577
+ quote_column_name(field)
578
+ else
579
+ field.to_s
580
+ end
581
+ # :nocov:
582
+ else
583
+ if quoted
584
+ "#{quote_table_name(table)}.#{quote_column_name(field)}"
585
+ else
586
+ # :nocov:
587
+ "#{table.to_s}.#{field.to_s}"
588
+ # :nocov:
589
+ end
590
+ end
591
+ end
592
+
593
+ def get_aliased_field(path_with_field, join_manager)
594
+ path = JSONAPI::Path.new(resource_klass: self, path_string: path_with_field)
595
+
596
+ relationship_segment = path.segments[-2]
597
+ field_segment = path.segments[-1]
598
+
599
+ if relationship_segment
600
+ join_details = join_manager.join_details[path.last_relationship]
601
+ table_alias = join_details[:alias]
602
+ else
603
+ table_alias = self._table_name
604
+ end
605
+
606
+ concat_table_field(table_alias, field_segment.delegated_field_name)
607
+ end
608
+
609
+ def apply_filter(records, filter, value, options = {})
610
+ strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
611
+
612
+ if strategy
613
+ records = call_method_or_proc(strategy, records, value, options)
614
+ else
615
+ join_manager = options.dig(:_relation_helper_options, :join_manager)
616
+ field = join_manager ? get_aliased_field(filter, join_manager) : filter.to_s
617
+ records = records.where(Arel.sql(field) => value)
618
+ end
619
+
620
+ records
621
+ end
622
+
623
+ def apply_filters(records, filters, options = {})
624
+ # required_includes = []
625
+
626
+ if filters
627
+ filters.each do |filter, value|
628
+ if _relationships.include?(filter) && _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply].blank?
629
+ if _relationships[filter].belongs_to?
630
+ records = apply_filter(records, _relationships[filter].foreign_key, value, options)
631
+ else
632
+ # required_includes.push(filter.to_s)
633
+ records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
634
+ end
635
+ else
636
+ records = apply_filter(records, filter, value, options)
637
+ end
638
+ end
639
+ end
640
+
641
+ # if required_includes.any?
642
+ # records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
643
+ # end
644
+
645
+ records
646
+ end
647
+
648
+ def filter_records(records, filters, options)
649
+ records = apply_filters(records, filters, options)
650
+ apply_includes(records, options)
651
+ end
652
+
653
+ def construct_order_options(sort_params)
654
+ sort_params ||= default_sort
655
+
656
+ return {} unless sort_params
657
+
658
+ sort_params.each_with_object({}) do |sort, order_hash|
659
+ field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
660
+ order_hash[field] = sort[:direction]
661
+ end
662
+ end
663
+
664
+ def sort_records(records, order_options, options = {})
665
+ apply_sort(records, order_options, options)
666
+ end
667
+
668
+ # Assumes ActiveRecord's counting. Override if you need a different counting method
669
+ def count_records(records)
670
+ records.count(:all)
671
+ end
672
+
673
+ def find_count(filters, options = {})
674
+ count_records(filter_records(records(options), filters, options))
675
+ end
676
+
677
+ def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
678
+ records = relationship.parent_resource.records_for_source_to_related(options)
679
+ strategy = relationship.options[:apply_join]
680
+
681
+ if strategy
682
+ records = call_method_or_proc(strategy, records, relationship, resource_type, join_type, options)
683
+ else
684
+ records = apply_join(records: records,
685
+ relationship: relationship,
686
+ resource_type: resource_type,
687
+ join_type: join_type,
688
+ options: options)
689
+ end
690
+
691
+ records
692
+ end
693
+
694
+ def join_relationship(records:, relationship:, resource_type: nil, join_type: :inner, options: {})
695
+ relationship_records = relationship_records(relationship: relationship,
696
+ join_type: join_type,
697
+ resource_type: resource_type,
698
+ options: options)
699
+ records.merge(relationship_records)
700
+ end
701
+
702
+ def warn_about_unused_methods
703
+ if Rails.env.development?
704
+ if !caching? && implements_class_method?(:records_for_populate)
705
+ warn "#{self}: The `records_for_populate` method is not used when caching is disabled."
706
+ end
707
+ end
708
+ end
709
+
710
+ def implements_class_method?(method_name)
711
+ methods(false).include?(method_name)
712
+ end
713
+ end
714
+ end
715
+ end