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,715 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module ActiveRelationRetrievalV09
|
5
|
+
def find_related_ids(relationship, options = {})
|
6
|
+
self.class.find_related_fragments(self.fragment, relationship, options).keys.collect { |rid| rid.id }
|
7
|
+
end
|
8
|
+
|
9
|
+
# Override this on a resource to customize how the associated records
|
10
|
+
# are fetched for a model. Particularly helpful for authorization.
|
11
|
+
def records_for(relation_name)
|
12
|
+
_model.public_send relation_name
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Finds Resources using the `filters`. Pagination and sort options are used when provided
|
17
|
+
#
|
18
|
+
# @param filters [Hash] the filters hash
|
19
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
20
|
+
# @option options [Hash] :sort_criteria The `sort criteria`
|
21
|
+
# @option options [Hash] :include_directives The `include_directives`
|
22
|
+
#
|
23
|
+
# @return [Array<Resource>] the Resource instances matching the filters, sorting and pagination rules.
|
24
|
+
def find(filters, options = {})
|
25
|
+
context = options[:context]
|
26
|
+
|
27
|
+
records = filter_records(records(options), filters, options)
|
28
|
+
|
29
|
+
sort_criteria = options.fetch(:sort_criteria) { [] }
|
30
|
+
order_options = construct_order_options(sort_criteria)
|
31
|
+
records = sort_records(records, order_options, context)
|
32
|
+
|
33
|
+
records = apply_pagination(records, options[:paginator], order_options)
|
34
|
+
|
35
|
+
resources_for(records, context)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Counts Resources found using the `filters`
|
39
|
+
#
|
40
|
+
# @param filters [Hash] the filters hash
|
41
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
42
|
+
#
|
43
|
+
# @return [Integer] the count
|
44
|
+
def count(filters, options = {})
|
45
|
+
count_records(filter_records(records(options), filters, options))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the single Resource identified by `key`
|
49
|
+
#
|
50
|
+
# @param key the primary key of the resource to find
|
51
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
52
|
+
def find_by_key(key, options = {})
|
53
|
+
context = options[:context]
|
54
|
+
records = records(options)
|
55
|
+
|
56
|
+
records = apply_includes(records, options)
|
57
|
+
model = records.where({_primary_key => key}).first
|
58
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
|
59
|
+
self.resource_klass_for_model(model).new(model, context)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns an array of Resources identified by the `keys` array
|
63
|
+
#
|
64
|
+
# @param keys [Array<key>] Array of primary keys to find resources for
|
65
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
66
|
+
def find_by_keys(keys, options = {})
|
67
|
+
context = options[:context]
|
68
|
+
records = records(options)
|
69
|
+
records = apply_includes(records, options)
|
70
|
+
models = records.where({_primary_key => keys})
|
71
|
+
models.collect do |model|
|
72
|
+
self.resource_klass_for_model(model).new(model, context)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns an array of Resources identified by the `keys` array. The resources are not filtered as this
|
77
|
+
# will have been done in a prior step
|
78
|
+
#
|
79
|
+
# @param keys [Array<key>] Array of primary keys to find resources for
|
80
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
81
|
+
def find_to_populate_by_keys(keys, options = {})
|
82
|
+
records = records_for_populate(options).where(_primary_key => keys)
|
83
|
+
resources_for(records, options[:context])
|
84
|
+
end
|
85
|
+
|
86
|
+
# Finds Resource fragments using the `filters`. Pagination and sort options are used when provided.
|
87
|
+
# Note: This is incompatible with Polymorphic resources (which are going to come from two separate tables)
|
88
|
+
#
|
89
|
+
# @param filters [Hash] the filters hash
|
90
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
91
|
+
# @option options [Hash] :sort_criteria The `sort criteria`
|
92
|
+
# @option options [Hash] :include_directives The `include_directives`
|
93
|
+
# @option options [Boolean] :cache Return the resources' cache field
|
94
|
+
#
|
95
|
+
# @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field}]
|
96
|
+
# the ResourceInstances matching the filters, sorting, and pagination rules along with any request
|
97
|
+
# additional_field values
|
98
|
+
def find_fragments(filters, options = {})
|
99
|
+
context = options[:context]
|
100
|
+
|
101
|
+
sort_criteria = options.fetch(:sort_criteria) { [] }
|
102
|
+
order_options = construct_order_options(sort_criteria)
|
103
|
+
|
104
|
+
join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
|
105
|
+
filters: filters,
|
106
|
+
sort_criteria: sort_criteria)
|
107
|
+
|
108
|
+
options[:_relation_helper_options] = { join_manager: join_manager, sort_fields: [] }
|
109
|
+
include_directives = options[:include_directives]
|
110
|
+
|
111
|
+
records = records(options)
|
112
|
+
|
113
|
+
records = apply_joins(records, join_manager, options)
|
114
|
+
|
115
|
+
records = filter_records(records, filters, options)
|
116
|
+
|
117
|
+
records = sort_records(records, order_options, context)
|
118
|
+
|
119
|
+
records = apply_pagination(records, options[:paginator], order_options)
|
120
|
+
|
121
|
+
resources = resources_for(records, context)
|
122
|
+
|
123
|
+
fragments = {}
|
124
|
+
|
125
|
+
linkage_relationships = to_one_relationships_for_linkage(include_directives.try(:[], :include_related))
|
126
|
+
|
127
|
+
resources.each do |resource|
|
128
|
+
rid = resource.identity
|
129
|
+
|
130
|
+
cache = options[:cache] ? resource.cache_field_value : nil
|
131
|
+
|
132
|
+
fragment = JSONAPI::ResourceFragment.new(rid, resource: resource, cache: cache, primary: true)
|
133
|
+
complete_linkages(fragment, linkage_relationships)
|
134
|
+
fragments[rid] ||= fragment
|
135
|
+
end
|
136
|
+
|
137
|
+
fragments
|
138
|
+
end
|
139
|
+
|
140
|
+
# Finds Resource Fragments related to the source resources through the specified relationship
|
141
|
+
#
|
142
|
+
# @param source_fragment [ResourceFragment>] The resource to find related ResourcesFragments for
|
143
|
+
# @param relationship_name [String | Symbol] The name of the relationship
|
144
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
145
|
+
# @option options [Boolean] :cache Return the resources' cache field
|
146
|
+
#
|
147
|
+
# @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, related: {relationship_name: [] }}}]
|
148
|
+
# the ResourceInstances matching the filters, sorting, and pagination rules along with any request
|
149
|
+
# additional_field values
|
150
|
+
def find_related_fragments(source_fragment, relationship, options)
|
151
|
+
fragments = {}
|
152
|
+
include_directives = options[:include_directives]
|
153
|
+
|
154
|
+
resource_klass = relationship.resource_klass
|
155
|
+
|
156
|
+
linkage_relationships = resource_klass.to_one_relationships_for_linkage(include_directives.try(:[], :include_related))
|
157
|
+
|
158
|
+
resources = source_fragment.resource.send(relationship.name, options)
|
159
|
+
resources = [] if resources.nil?
|
160
|
+
resources = [resources] unless resources.is_a?(Array)
|
161
|
+
|
162
|
+
# Do not pass in source as it will setup linkage data to the source
|
163
|
+
load_resources_to_fragments(fragments, resources, nil, relationship, linkage_relationships, options)
|
164
|
+
|
165
|
+
fragments
|
166
|
+
end
|
167
|
+
|
168
|
+
def find_included_fragments(source_fragments, relationship, options)
|
169
|
+
fragments = {}
|
170
|
+
include_directives = options[:include_directives]
|
171
|
+
resource_klass = relationship.resource_klass
|
172
|
+
|
173
|
+
linkage_relationships = if relationship.polymorphic?
|
174
|
+
[]
|
175
|
+
else
|
176
|
+
resource_klass.to_one_relationships_for_linkage(include_directives.try(:[], :include_related))
|
177
|
+
end
|
178
|
+
|
179
|
+
source_fragments.each do |source_fragment|
|
180
|
+
raise "Missing resource in fragment #{__callee__}" unless source_fragment.resource.present?
|
181
|
+
|
182
|
+
resources = source_fragment.resource.send(relationship.name, options.except(:sort_criteria))
|
183
|
+
resources = [] if resources.nil?
|
184
|
+
resources = [resources] unless resources.is_a?(Array)
|
185
|
+
|
186
|
+
load_resources_to_fragments(fragments, resources, source_fragment, relationship, linkage_relationships, options)
|
187
|
+
end
|
188
|
+
|
189
|
+
fragments
|
190
|
+
end
|
191
|
+
|
192
|
+
def find_related_fragments_from_inverse(source, source_relationship, options, connect_source_identity)
|
193
|
+
raise "Not Implemented #{__callee__}"
|
194
|
+
end
|
195
|
+
|
196
|
+
# Counts Resources related to the source resource through the specified relationship
|
197
|
+
#
|
198
|
+
# @param source_rid [ResourceIdentity] Source resource identifier
|
199
|
+
# @param relationship_name [String | Symbol] The name of the relationship
|
200
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
201
|
+
#
|
202
|
+
# @return [Integer] the count
|
203
|
+
|
204
|
+
def count_related(source, relationship, options = {})
|
205
|
+
opts = options.except(:paginator)
|
206
|
+
|
207
|
+
related_resource_records = source.public_send("records_for_#{relationship.name}",
|
208
|
+
opts)
|
209
|
+
count_records(related_resource_records)
|
210
|
+
end
|
211
|
+
|
212
|
+
# This resource class (ActiveRelationResource) uses an `ActiveRecord::Relation` as the starting point for
|
213
|
+
# retrieving models. From this relation filters, sorts and joins are applied as needed.
|
214
|
+
# Depending on which phase of the request processing different `records` methods will be called, giving the user
|
215
|
+
# the opportunity to override them differently for performance and security reasons.
|
216
|
+
|
217
|
+
# begin `records`methods
|
218
|
+
|
219
|
+
# Base for the `records` methods that follow and is not directly used for accessing model data by this class.
|
220
|
+
# Overriding this method gives a single place to affect the `ActiveRecord::Relation` used for the resource.
|
221
|
+
#
|
222
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
223
|
+
#
|
224
|
+
# @return [ActiveRecord::Relation]
|
225
|
+
def records_base(_options = {})
|
226
|
+
_model_class.all
|
227
|
+
end
|
228
|
+
|
229
|
+
# The `ActiveRecord::Relation` used for finding user requested models. This may be overridden to enforce
|
230
|
+
# permissions checks on the request.
|
231
|
+
#
|
232
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
233
|
+
#
|
234
|
+
# @return [ActiveRecord::Relation]
|
235
|
+
def records(options = {})
|
236
|
+
records_base(options)
|
237
|
+
end
|
238
|
+
|
239
|
+
# The `ActiveRecord::Relation` used for populating the ResourceSet. Only resources that have been previously
|
240
|
+
# identified through the `records` method will be accessed. Thus it should not be necessary to reapply permissions
|
241
|
+
# checks. However if the model needs to include other models adding `includes` is appropriate
|
242
|
+
#
|
243
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
244
|
+
#
|
245
|
+
# @return [ActiveRecord::Relation]
|
246
|
+
def records_for_populate(options = {})
|
247
|
+
records_base(options)
|
248
|
+
end
|
249
|
+
|
250
|
+
# The `ActiveRecord::Relation` used for the finding related resources.
|
251
|
+
#
|
252
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
253
|
+
#
|
254
|
+
# @return [ActiveRecord::Relation]
|
255
|
+
def records_for_source_to_related(options = {})
|
256
|
+
records_base(options)
|
257
|
+
end
|
258
|
+
|
259
|
+
# end `records` methods
|
260
|
+
|
261
|
+
def load_resources_to_fragments(fragments, related_resources, source_resource, source_relationship, linkage_relationships, options)
|
262
|
+
cached = options[:cache]
|
263
|
+
primary = source_resource.nil?
|
264
|
+
|
265
|
+
related_resources.each do |related_resource|
|
266
|
+
cache = cached ? related_resource.cache_field_value : nil
|
267
|
+
|
268
|
+
fragment = fragments[related_resource.identity]
|
269
|
+
|
270
|
+
if fragment.nil?
|
271
|
+
fragment = JSONAPI::ResourceFragment.new(related_resource.identity,
|
272
|
+
resource: related_resource,
|
273
|
+
cache: cache,
|
274
|
+
primary: primary)
|
275
|
+
|
276
|
+
fragments[related_resource.identity] = fragment
|
277
|
+
complete_linkages(fragment, linkage_relationships)
|
278
|
+
end
|
279
|
+
|
280
|
+
if source_resource
|
281
|
+
source_resource.add_related_identity(source_relationship.name, related_resource.identity)
|
282
|
+
fragment.add_related_from(source_resource.identity)
|
283
|
+
fragment.add_related_identity(source_relationship.inverse_relationship, source_resource.identity)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def complete_linkages(fragment, linkage_relationships)
|
289
|
+
linkage_relationships.each do |linkage_relationship|
|
290
|
+
related_id = fragment.resource._model.attributes[linkage_relationship.foreign_key.to_s]
|
291
|
+
|
292
|
+
related_rid = if related_id
|
293
|
+
if linkage_relationship.polymorphic?
|
294
|
+
related_type = fragment.resource._model.attributes[linkage_relationship.polymorphic_type]
|
295
|
+
JSONAPI::ResourceIdentity.new(Resource.resource_klass_for(related_type), related_id)
|
296
|
+
else
|
297
|
+
klass = linkage_relationship.resource_klass
|
298
|
+
JSONAPI::ResourceIdentity.new(klass, related_id)
|
299
|
+
end
|
300
|
+
else
|
301
|
+
nil
|
302
|
+
end
|
303
|
+
|
304
|
+
fragment.add_related_identity(linkage_relationship.name, related_rid)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def apply_join(records:, relationship:, resource_type:, join_type:, options:)
|
309
|
+
if relationship.polymorphic? && relationship.belongs_to?
|
310
|
+
case join_type
|
311
|
+
when :inner
|
312
|
+
records = records.joins(resource_type.to_s.singularize.to_sym)
|
313
|
+
when :left
|
314
|
+
records = records.joins_left(resource_type.to_s.singularize.to_sym)
|
315
|
+
end
|
316
|
+
else
|
317
|
+
relation_name = relationship.relation_name(options)
|
318
|
+
|
319
|
+
# if relationship.alias_on_join
|
320
|
+
# alias_name = "#{relationship.preferred_alias}_#{relation_name}"
|
321
|
+
# case join_type
|
322
|
+
# when :inner
|
323
|
+
# records = records.joins_with_alias(relation_name, alias_name)
|
324
|
+
# when :left
|
325
|
+
# records = records.left_joins_with_alias(relation_name, alias_name)
|
326
|
+
# end
|
327
|
+
# else
|
328
|
+
case join_type
|
329
|
+
when :inner
|
330
|
+
records = records.joins(relation_name)
|
331
|
+
when :left
|
332
|
+
records = records.left_joins(relation_name)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
# end
|
336
|
+
records
|
337
|
+
end
|
338
|
+
|
339
|
+
def define_relationship_methods(relationship_name, relationship_klass, options)
|
340
|
+
foreign_key = super
|
341
|
+
|
342
|
+
relationship = _relationship(relationship_name)
|
343
|
+
|
344
|
+
case relationship
|
345
|
+
when JSONAPI::Relationship::ToOne
|
346
|
+
associated = define_resource_relationship_accessor(:one, relationship_name)
|
347
|
+
args = [relationship, foreign_key, associated, relationship_name]
|
348
|
+
|
349
|
+
relationship.belongs_to? ? build_belongs_to(*args) : build_has_one(*args)
|
350
|
+
when JSONAPI::Relationship::ToMany
|
351
|
+
associated = define_resource_relationship_accessor(:many, relationship_name)
|
352
|
+
|
353
|
+
build_to_many(relationship, foreign_key, associated, relationship_name)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
def define_resource_relationship_accessor(type, relationship_name)
|
359
|
+
associated_records_method_name = {
|
360
|
+
one: "record_for_#{relationship_name}",
|
361
|
+
many: "records_for_#{relationship_name}"
|
362
|
+
}.fetch(type)
|
363
|
+
|
364
|
+
define_on_resource associated_records_method_name do |options = {}|
|
365
|
+
relationship = self.class._relationships[relationship_name]
|
366
|
+
relation_name = relationship.relation_name(context: @context)
|
367
|
+
records = records_for(relation_name)
|
368
|
+
|
369
|
+
resource_klass = relationship.resource_klass
|
370
|
+
|
371
|
+
include_directives = options[:include_directives]&.include_directives&.dig(relationship_name)
|
372
|
+
|
373
|
+
options = options.dup
|
374
|
+
options[:include_directives] = include_directives
|
375
|
+
|
376
|
+
records = resource_klass.apply_includes(records, options)
|
377
|
+
|
378
|
+
filters = options.fetch(:filters, {})
|
379
|
+
unless filters.nil? || filters.empty?
|
380
|
+
records = resource_klass.apply_filters(records, filters, options)
|
381
|
+
end
|
382
|
+
|
383
|
+
sort_criteria = options.fetch(:sort_criteria, {})
|
384
|
+
order_options = relationship.resource_klass.construct_order_options(sort_criteria)
|
385
|
+
records = resource_klass.apply_sort(records, order_options, options)
|
386
|
+
|
387
|
+
paginator = options[:paginator]
|
388
|
+
if paginator
|
389
|
+
records = resource_klass.apply_pagination(records, paginator, order_options)
|
390
|
+
end
|
391
|
+
|
392
|
+
records
|
393
|
+
end
|
394
|
+
|
395
|
+
associated_records_method_name
|
396
|
+
end
|
397
|
+
|
398
|
+
def build_belongs_to(relationship, foreign_key, associated_records_method_name, relationship_name)
|
399
|
+
# Calls method matching foreign key name on model instance
|
400
|
+
define_on_resource foreign_key do
|
401
|
+
@model.method(foreign_key).call
|
402
|
+
end
|
403
|
+
|
404
|
+
# Returns instantiated related resource object or nil
|
405
|
+
define_on_resource relationship_name do |options = {}|
|
406
|
+
relationship = self.class._relationships[relationship_name]
|
407
|
+
|
408
|
+
if relationship.polymorphic?
|
409
|
+
associated_model = public_send(associated_records_method_name)
|
410
|
+
resource_klass = self.class.resource_klass_for_model(associated_model) if associated_model
|
411
|
+
return resource_klass.new(associated_model, @context) if resource_klass
|
412
|
+
else
|
413
|
+
resource_klass = relationship.resource_klass
|
414
|
+
if resource_klass
|
415
|
+
associated_model = public_send(associated_records_method_name)
|
416
|
+
return associated_model ? resource_klass.new(associated_model, @context) : nil
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def build_has_one(relationship, foreign_key, associated_records_method_name, relationship_name)
|
423
|
+
# Returns primary key name of related resource class
|
424
|
+
define_on_resource foreign_key do
|
425
|
+
relationship = self.class._relationships[relationship_name]
|
426
|
+
|
427
|
+
record = public_send(associated_records_method_name)
|
428
|
+
return nil if record.nil?
|
429
|
+
record.public_send(relationship.resource_klass._primary_key)
|
430
|
+
end
|
431
|
+
|
432
|
+
# Returns instantiated related resource object or nil
|
433
|
+
define_on_resource relationship_name do |options = {}|
|
434
|
+
relationship = self.class._relationships[relationship_name]
|
435
|
+
|
436
|
+
if relationship.polymorphic?
|
437
|
+
associated_model = public_send(associated_records_method_name)
|
438
|
+
resource_klass = self.class.resource_klass_for_model(associated_model) if associated_model
|
439
|
+
return resource_klass.new(associated_model, @context) if resource_klass && associated_model
|
440
|
+
else
|
441
|
+
resource_klass = relationship.resource_klass
|
442
|
+
if resource_klass
|
443
|
+
associated_model = public_send(associated_records_method_name)
|
444
|
+
return associated_model ? resource_klass.new(associated_model, @context) : nil
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def build_to_many(relationship, foreign_key, associated_records_method_name, relationship_name)
|
451
|
+
# Returns array of primary keys of related resource classes
|
452
|
+
define_on_resource foreign_key do
|
453
|
+
records = public_send(associated_records_method_name)
|
454
|
+
return records.collect do |record|
|
455
|
+
record.public_send(relationship.resource_klass._primary_key)
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Returns array of instantiated related resource objects
|
460
|
+
define_on_resource relationship_name do |options = {}|
|
461
|
+
relationship = self.class._relationships[relationship_name]
|
462
|
+
|
463
|
+
resource_klass = relationship.resource_klass
|
464
|
+
records = public_send(associated_records_method_name, options)
|
465
|
+
|
466
|
+
return records.collect do |record|
|
467
|
+
if relationship.polymorphic?
|
468
|
+
resource_klass = self.class.resource_for_model(record)
|
469
|
+
end
|
470
|
+
resource_klass.new(record, @context)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
|
476
|
+
case model_includes
|
477
|
+
when Array
|
478
|
+
return model_includes.map do |value|
|
479
|
+
resolve_relationship_names_to_relations(resource_klass, value, options)
|
480
|
+
end
|
481
|
+
when Hash
|
482
|
+
model_includes.keys.each do |key|
|
483
|
+
relationship = resource_klass._relationships[key]
|
484
|
+
value = model_includes[key]
|
485
|
+
model_includes.delete(key)
|
486
|
+
model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
|
487
|
+
end
|
488
|
+
return model_includes
|
489
|
+
when Symbol
|
490
|
+
relationship = resource_klass._relationships[model_includes]
|
491
|
+
return relationship.relation_name(options)
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
def apply_includes(records, options = {})
|
496
|
+
include_directives = options[:include_directives]
|
497
|
+
if include_directives
|
498
|
+
model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
|
499
|
+
records = records.includes(model_includes)
|
500
|
+
end
|
501
|
+
|
502
|
+
records
|
503
|
+
end
|
504
|
+
|
505
|
+
def apply_joins(records, join_manager, options)
|
506
|
+
join_manager.join(records, options)
|
507
|
+
end
|
508
|
+
|
509
|
+
def apply_pagination(records, paginator, order_options)
|
510
|
+
records = paginator.apply(records, order_options) if paginator
|
511
|
+
records
|
512
|
+
end
|
513
|
+
|
514
|
+
def apply_sort(records, order_options, options)
|
515
|
+
if order_options.any?
|
516
|
+
order_options.each_pair do |field, direction|
|
517
|
+
records = apply_single_sort(records, field, direction, options)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
records
|
522
|
+
end
|
523
|
+
|
524
|
+
def apply_single_sort(records, field, direction, options)
|
525
|
+
strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]
|
526
|
+
|
527
|
+
delegated_field = attribute_to_model_field(field)
|
528
|
+
|
529
|
+
options[:_relation_helper_options] ||= {}
|
530
|
+
options[:_relation_helper_options][:sort_fields] ||= []
|
531
|
+
|
532
|
+
if strategy
|
533
|
+
records = call_method_or_proc(strategy, records, direction, options)
|
534
|
+
else
|
535
|
+
join_manager = options.dig(:_relation_helper_options, :join_manager)
|
536
|
+
sort_field = join_manager ? get_aliased_field(delegated_field[:name], join_manager) : delegated_field[:name]
|
537
|
+
options[:_relation_helper_options][:sort_fields].push("#{sort_field}")
|
538
|
+
records = records.order(Arel.sql("#{sort_field} #{direction}"))
|
539
|
+
end
|
540
|
+
records
|
541
|
+
end
|
542
|
+
|
543
|
+
def _lookup_association_chain(model_names)
|
544
|
+
associations = []
|
545
|
+
model_names.inject do |prev, current|
|
546
|
+
association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
|
547
|
+
assoc.name.to_s.underscore == current.underscore
|
548
|
+
end
|
549
|
+
associations << association
|
550
|
+
association.class_name
|
551
|
+
end
|
552
|
+
|
553
|
+
associations
|
554
|
+
end
|
555
|
+
|
556
|
+
def _build_joins(associations)
|
557
|
+
joins = []
|
558
|
+
|
559
|
+
associations.inject do |prev, current|
|
560
|
+
joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
|
561
|
+
current
|
562
|
+
end
|
563
|
+
joins.join("\n")
|
564
|
+
end
|
565
|
+
|
566
|
+
def concat_table_field(table, field, quoted = false)
|
567
|
+
if table.blank?
|
568
|
+
split_table, split_field = field.to_s.split('.')
|
569
|
+
if split_table && split_field
|
570
|
+
table = split_table
|
571
|
+
field = split_field
|
572
|
+
end
|
573
|
+
end
|
574
|
+
if table.blank?
|
575
|
+
# :nocov:
|
576
|
+
if quoted
|
577
|
+
quote_column_name(field)
|
578
|
+
else
|
579
|
+
field.to_s
|
580
|
+
end
|
581
|
+
# :nocov:
|
582
|
+
else
|
583
|
+
if quoted
|
584
|
+
"#{quote_table_name(table)}.#{quote_column_name(field)}"
|
585
|
+
else
|
586
|
+
# :nocov:
|
587
|
+
"#{table.to_s}.#{field.to_s}"
|
588
|
+
# :nocov:
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
def get_aliased_field(path_with_field, join_manager)
|
594
|
+
path = JSONAPI::Path.new(resource_klass: self, path_string: path_with_field)
|
595
|
+
|
596
|
+
relationship_segment = path.segments[-2]
|
597
|
+
field_segment = path.segments[-1]
|
598
|
+
|
599
|
+
if relationship_segment
|
600
|
+
join_details = join_manager.join_details[path.last_relationship]
|
601
|
+
table_alias = join_details[:alias]
|
602
|
+
else
|
603
|
+
table_alias = self._table_name
|
604
|
+
end
|
605
|
+
|
606
|
+
concat_table_field(table_alias, field_segment.delegated_field_name)
|
607
|
+
end
|
608
|
+
|
609
|
+
def apply_filter(records, filter, value, options = {})
|
610
|
+
strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
|
611
|
+
|
612
|
+
if strategy
|
613
|
+
records = call_method_or_proc(strategy, records, value, options)
|
614
|
+
else
|
615
|
+
join_manager = options.dig(:_relation_helper_options, :join_manager)
|
616
|
+
field = join_manager ? get_aliased_field(filter, join_manager) : filter.to_s
|
617
|
+
records = records.where(Arel.sql(field) => value)
|
618
|
+
end
|
619
|
+
|
620
|
+
records
|
621
|
+
end
|
622
|
+
|
623
|
+
def apply_filters(records, filters, options = {})
|
624
|
+
# required_includes = []
|
625
|
+
|
626
|
+
if filters
|
627
|
+
filters.each do |filter, value|
|
628
|
+
if _relationships.include?(filter) && _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply].blank?
|
629
|
+
if _relationships[filter].belongs_to?
|
630
|
+
records = apply_filter(records, _relationships[filter].foreign_key, value, options)
|
631
|
+
else
|
632
|
+
# required_includes.push(filter.to_s)
|
633
|
+
records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
|
634
|
+
end
|
635
|
+
else
|
636
|
+
records = apply_filter(records, filter, value, options)
|
637
|
+
end
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
# if required_includes.any?
|
642
|
+
# records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
|
643
|
+
# end
|
644
|
+
|
645
|
+
records
|
646
|
+
end
|
647
|
+
|
648
|
+
def filter_records(records, filters, options)
|
649
|
+
records = apply_filters(records, filters, options)
|
650
|
+
apply_includes(records, options)
|
651
|
+
end
|
652
|
+
|
653
|
+
def construct_order_options(sort_params)
|
654
|
+
sort_params ||= default_sort
|
655
|
+
|
656
|
+
return {} unless sort_params
|
657
|
+
|
658
|
+
sort_params.each_with_object({}) do |sort, order_hash|
|
659
|
+
field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
|
660
|
+
order_hash[field] = sort[:direction]
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
def sort_records(records, order_options, options = {})
|
665
|
+
apply_sort(records, order_options, options)
|
666
|
+
end
|
667
|
+
|
668
|
+
# Assumes ActiveRecord's counting. Override if you need a different counting method
|
669
|
+
def count_records(records)
|
670
|
+
records.count(:all)
|
671
|
+
end
|
672
|
+
|
673
|
+
def find_count(filters, options = {})
|
674
|
+
count_records(filter_records(records(options), filters, options))
|
675
|
+
end
|
676
|
+
|
677
|
+
def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
|
678
|
+
records = relationship.parent_resource.records_for_source_to_related(options)
|
679
|
+
strategy = relationship.options[:apply_join]
|
680
|
+
|
681
|
+
if strategy
|
682
|
+
records = call_method_or_proc(strategy, records, relationship, resource_type, join_type, options)
|
683
|
+
else
|
684
|
+
records = apply_join(records: records,
|
685
|
+
relationship: relationship,
|
686
|
+
resource_type: resource_type,
|
687
|
+
join_type: join_type,
|
688
|
+
options: options)
|
689
|
+
end
|
690
|
+
|
691
|
+
records
|
692
|
+
end
|
693
|
+
|
694
|
+
def join_relationship(records:, relationship:, resource_type: nil, join_type: :inner, options: {})
|
695
|
+
relationship_records = relationship_records(relationship: relationship,
|
696
|
+
join_type: join_type,
|
697
|
+
resource_type: resource_type,
|
698
|
+
options: options)
|
699
|
+
records.merge(relationship_records)
|
700
|
+
end
|
701
|
+
|
702
|
+
def warn_about_unused_methods
|
703
|
+
if Rails.env.development?
|
704
|
+
if !caching? && implements_class_method?(:records_for_populate)
|
705
|
+
warn "#{self}: The `records_for_populate` method is not used when caching is disabled."
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
def implements_class_method?(method_name)
|
711
|
+
methods(false).include?(method_name)
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|