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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +39 -2
- data/lib/generators/jsonapi/controller_generator.rb +2 -0
- data/lib/generators/jsonapi/resource_generator.rb +2 -0
- data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +3 -2
- data/lib/jsonapi/active_relation/join_manager.rb +30 -18
- data/lib/jsonapi/active_relation/join_manager_v10.rb +305 -0
- data/lib/jsonapi/active_relation_retrieval.rb +885 -0
- data/lib/jsonapi/active_relation_retrieval_v09.rb +715 -0
- data/lib/jsonapi/{active_relation_resource.rb → active_relation_retrieval_v10.rb} +113 -135
- data/lib/jsonapi/acts_as_resource_controller.rb +49 -49
- data/lib/jsonapi/cached_response_fragment.rb +4 -2
- data/lib/jsonapi/callbacks.rb +2 -0
- data/lib/jsonapi/compiled_json.rb +2 -0
- data/lib/jsonapi/configuration.rb +35 -15
- data/lib/jsonapi/error.rb +2 -0
- data/lib/jsonapi/error_codes.rb +2 -0
- data/lib/jsonapi/exceptions.rb +2 -0
- data/lib/jsonapi/formatter.rb +2 -0
- data/lib/jsonapi/include_directives.rb +77 -19
- data/lib/jsonapi/link_builder.rb +2 -0
- data/lib/jsonapi/mime_types.rb +6 -10
- data/lib/jsonapi/naive_cache.rb +2 -0
- data/lib/jsonapi/operation.rb +2 -0
- data/lib/jsonapi/operation_result.rb +2 -0
- data/lib/jsonapi/paginator.rb +2 -0
- data/lib/jsonapi/path.rb +2 -0
- data/lib/jsonapi/path_segment.rb +4 -2
- data/lib/jsonapi/processor.rb +95 -140
- data/lib/jsonapi/relationship.rb +89 -35
- data/lib/jsonapi/{request_parser.rb → request.rb} +157 -164
- data/lib/jsonapi/resource.rb +7 -2
- data/lib/jsonapi/{basic_resource.rb → resource_common.rb} +187 -88
- data/lib/jsonapi/resource_controller.rb +2 -0
- data/lib/jsonapi/resource_controller_metal.rb +2 -0
- data/lib/jsonapi/resource_fragment.rb +17 -15
- data/lib/jsonapi/resource_identity.rb +6 -0
- data/lib/jsonapi/resource_serializer.rb +20 -4
- data/lib/jsonapi/resource_set.rb +36 -16
- data/lib/jsonapi/resource_tree.rb +191 -0
- data/lib/jsonapi/resources/railtie.rb +3 -1
- data/lib/jsonapi/resources/version.rb +3 -1
- data/lib/jsonapi/response_document.rb +4 -2
- data/lib/jsonapi/routing_ext.rb +4 -2
- data/lib/jsonapi/simple_resource.rb +13 -0
- data/lib/jsonapi-resources.rb +10 -4
- data/lib/tasks/check_upgrade.rake +3 -1
- metadata +47 -15
- 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
|