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,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