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