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