jsonapi-resources 0.10.7 → 0.11.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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 -17
  28. data/lib/jsonapi/path.rb +2 -0
  29. data/lib/jsonapi/path_segment.rb +4 -2
  30. data/lib/jsonapi/processor.rb +100 -153
  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