jsonapi-resources 0.10.0.beta3 → 0.10.0.beta4
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/lib/jsonapi-resources.rb +4 -2
- data/lib/jsonapi/{active_relation_resource_finder → active_relation}/adapters/join_left_active_record_adapter.rb +1 -1
- data/lib/jsonapi/{active_relation_resource_finder → active_relation}/join_manager.rb +1 -1
- data/lib/jsonapi/{active_relation_resource_finder.rb → active_relation_resource.rb} +96 -44
- data/lib/jsonapi/acts_as_resource_controller.rb +1 -1
- data/lib/jsonapi/basic_resource.rb +1137 -0
- data/lib/jsonapi/configuration.rb +4 -12
- data/lib/jsonapi/include_directives.rb +6 -8
- data/lib/jsonapi/link_builder.rb +100 -77
- data/lib/jsonapi/operation_result.rb +2 -2
- data/lib/jsonapi/processor.rb +6 -7
- data/lib/jsonapi/relationship.rb +26 -3
- data/lib/jsonapi/request_parser.rb +44 -10
- data/lib/jsonapi/resource.rb +3 -1106
- data/lib/jsonapi/resource_serializer.rb +35 -31
- data/lib/jsonapi/resource_set.rb +1 -1
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +7 -2
- data/lib/jsonapi/routing_ext.rb +16 -4
- metadata +22 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a67d9ec13b0f5c2ae85187c5a3511eebc1e74c0d94cd17109f09115af320aa8
|
4
|
+
data.tar.gz: 6526b529feaea2bdd37befe22b35bf994c469b31c884a85e20f1b67f7365c728
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81b16e8293752c7082bd572a3c763ff5b710181860153226c035dca100e52bf4b49ea8c4fa7495418b3657b642455eb889dea88fd4646c95ec1cb88d0fc59756
|
7
|
+
data.tar.gz: 44784c1244cfe58a0151246f5076250f0a296ba69eb67d6250e700a227955ad7e006ed670f72700981668f659e462990033bf555ecc0e86b61e65deeca2351be
|
data/lib/jsonapi-resources.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'jsonapi/resources/railtie'
|
2
2
|
require 'jsonapi/naive_cache'
|
3
3
|
require 'jsonapi/compiled_json'
|
4
|
+
require 'jsonapi/basic_resource'
|
5
|
+
require 'jsonapi/active_relation_resource'
|
4
6
|
require 'jsonapi/resource'
|
5
7
|
require 'jsonapi/cached_response_fragment'
|
6
8
|
require 'jsonapi/response_document'
|
@@ -25,8 +27,8 @@ require 'jsonapi/operation'
|
|
25
27
|
require 'jsonapi/operation_result'
|
26
28
|
require 'jsonapi/callbacks'
|
27
29
|
require 'jsonapi/link_builder'
|
28
|
-
require 'jsonapi/
|
29
|
-
require 'jsonapi/
|
30
|
+
require 'jsonapi/active_relation/adapters/join_left_active_record_adapter'
|
31
|
+
require 'jsonapi/active_relation/join_manager'
|
30
32
|
require 'jsonapi/resource_identity'
|
31
33
|
require 'jsonapi/resource_fragment'
|
32
34
|
require 'jsonapi/resource_id_tree'
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module JSONAPI
|
2
|
-
module
|
2
|
+
module ActiveRelation
|
3
3
|
|
4
4
|
# Stores relationship paths starting from the resource_klass, consolidating duplicate paths from
|
5
5
|
# relationships, filters and sorts. When joins are made the table aliases are tracked in join_details
|
@@ -1,13 +1,8 @@
|
|
1
|
-
require 'jsonapi/active_relation_resource_finder/adapters/join_left_active_record_adapter'
|
2
|
-
|
3
1
|
module JSONAPI
|
4
|
-
|
5
|
-
|
6
|
-
base.extend ClassMethods
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
2
|
+
class ActiveRelationResource < BasicResource
|
3
|
+
root_resource
|
10
4
|
|
5
|
+
class << self
|
11
6
|
# Finds Resources using the `filters`. Pagination and sort options are used when provided
|
12
7
|
#
|
13
8
|
# @param filters [Hash] the filters hash
|
@@ -19,9 +14,9 @@ module JSONAPI
|
|
19
14
|
def find(filters, options = {})
|
20
15
|
sort_criteria = options.fetch(:sort_criteria) { [] }
|
21
16
|
|
22
|
-
join_manager = JoinManager.new(resource_klass: self,
|
23
|
-
|
24
|
-
|
17
|
+
join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
|
18
|
+
filters: filters,
|
19
|
+
sort_criteria: sort_criteria)
|
25
20
|
|
26
21
|
paginator = options[:paginator]
|
27
22
|
|
@@ -41,8 +36,8 @@ module JSONAPI
|
|
41
36
|
#
|
42
37
|
# @return [Integer] the count
|
43
38
|
def count(filters, options = {})
|
44
|
-
join_manager = JoinManager.new(resource_klass: self,
|
45
|
-
|
39
|
+
join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
|
40
|
+
filters: filters)
|
46
41
|
|
47
42
|
records = apply_request_settings_to_records(records: records(options),
|
48
43
|
filters: filters,
|
@@ -70,6 +65,16 @@ module JSONAPI
|
|
70
65
|
resources_for(records, options[:context])
|
71
66
|
end
|
72
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
|
+
|
73
78
|
# Finds Resource fragments using the `filters`. Pagination and sort options are used when provided.
|
74
79
|
# Retrieving the ResourceIdentities and attributes does not instantiate a model instance.
|
75
80
|
# Note: This is incompatible with Polymorphic resources (which are going to come from two separate tables)
|
@@ -91,11 +96,11 @@ module JSONAPI
|
|
91
96
|
|
92
97
|
sort_criteria = options.fetch(:sort_criteria) { [] }
|
93
98
|
|
94
|
-
join_manager = JoinManager.new(resource_klass: resource_klass,
|
95
|
-
|
96
|
-
|
97
|
-
|
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)
|
99
104
|
|
100
105
|
paginator = options[:paginator]
|
101
106
|
|
@@ -149,7 +154,7 @@ module JSONAPI
|
|
149
154
|
end
|
150
155
|
|
151
156
|
fragments = {}
|
152
|
-
rows = records.
|
157
|
+
rows = records.pluck(*pluck_fields)
|
153
158
|
rows.collect do |row|
|
154
159
|
rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0])
|
155
160
|
|
@@ -224,9 +229,9 @@ module JSONAPI
|
|
224
229
|
filters = options.fetch(:filters, {})
|
225
230
|
|
226
231
|
# Joins in this case are related to the related_klass
|
227
|
-
join_manager = JoinManager.new(resource_klass: self,
|
228
|
-
|
229
|
-
|
232
|
+
join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
|
233
|
+
source_relationship: relationship,
|
234
|
+
filters: filters)
|
230
235
|
|
231
236
|
records = apply_request_settings_to_records(records: records(options),
|
232
237
|
resource_klass: related_klass,
|
@@ -242,10 +247,57 @@ module JSONAPI
|
|
242
247
|
count_records(records)
|
243
248
|
end
|
244
249
|
|
245
|
-
|
250
|
+
# This resource class (ActiveRelationResource) uses an `ActiveRecord::Relation` as the starting point for
|
251
|
+
# retrieving models. From this relation filters, sorts and joins are applied as needed.
|
252
|
+
# Depending on which phase of the request processing different `records` methods will be called, giving the user
|
253
|
+
# the opportunity to override them differently for performance and security reasons.
|
254
|
+
|
255
|
+
# begin `records`methods
|
256
|
+
|
257
|
+
# Base for the `records` methods that follow and is not directly used for accessing model data by this class.
|
258
|
+
# Overriding this method gives a single place to affect the `ActiveRecord::Relation` used for the resource.
|
259
|
+
#
|
260
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
261
|
+
#
|
262
|
+
# @return [ActiveRecord::Relation]
|
263
|
+
def records_base(_options = {})
|
246
264
|
_model_class.all
|
247
265
|
end
|
248
266
|
|
267
|
+
# The `ActiveRecord::Relation` used for finding user requested models. This may be overridden to enforce
|
268
|
+
# permissions checks on the request.
|
269
|
+
#
|
270
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
271
|
+
#
|
272
|
+
# @return [ActiveRecord::Relation]
|
273
|
+
def records(options = {})
|
274
|
+
records_base(options)
|
275
|
+
end
|
276
|
+
|
277
|
+
# The `ActiveRecord::Relation` used for populating the ResourceSet. Only resources that have been previously
|
278
|
+
# identified through the `records` method will be accessed. Thus it should not be necessary to reapply permissions
|
279
|
+
# checks. However if the model needs to include other models adding `includes` is appropriate
|
280
|
+
#
|
281
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
282
|
+
#
|
283
|
+
# @return [ActiveRecord::Relation]
|
284
|
+
def records_for_populate(options = {})
|
285
|
+
records_base(options)
|
286
|
+
end
|
287
|
+
|
288
|
+
# The `ActiveRecord::Relation` used for the finding related resources. Only resources that have been previously
|
289
|
+
# identified through the `records` method will be accessed and used as the basis to find related resources. Thus
|
290
|
+
# it should not be necessary to reapply permissions checks.
|
291
|
+
#
|
292
|
+
# @option options [Hash] :context The context of the request, set in the controller
|
293
|
+
#
|
294
|
+
# @return [ActiveRecord::Relation]
|
295
|
+
def records_for_source_to_related(options = {})
|
296
|
+
records_base(options)
|
297
|
+
end
|
298
|
+
|
299
|
+
# end `records` methods
|
300
|
+
|
249
301
|
def apply_join(records:, relationship:, resource_type:, join_type:, options:)
|
250
302
|
if relationship.polymorphic? && relationship.belongs_to?
|
251
303
|
case join_type
|
@@ -267,7 +319,7 @@ module JSONAPI
|
|
267
319
|
end
|
268
320
|
|
269
321
|
def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
|
270
|
-
records = relationship.parent_resource.
|
322
|
+
records = relationship.parent_resource.records_for_source_to_related(options)
|
271
323
|
strategy = relationship.options[:apply_join]
|
272
324
|
|
273
325
|
if strategy
|
@@ -328,15 +380,15 @@ module JSONAPI
|
|
328
380
|
sort_criteria << { field: field, direction: sort[:direction] }
|
329
381
|
end
|
330
382
|
|
331
|
-
join_manager = JoinManager.new(resource_klass: self,
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
383
|
+
join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
|
384
|
+
source_relationship: relationship,
|
385
|
+
relationships: linkage_relationships,
|
386
|
+
sort_criteria: sort_criteria,
|
387
|
+
filters: filters)
|
336
388
|
|
337
389
|
paginator = options[:paginator] if source_rids.count == 1
|
338
390
|
|
339
|
-
records = apply_request_settings_to_records(records:
|
391
|
+
records = apply_request_settings_to_records(records: records_for_source_to_related(options),
|
340
392
|
resource_klass: resource_klass,
|
341
393
|
sort_criteria: sort_criteria,
|
342
394
|
primary_keys: source_ids,
|
@@ -454,16 +506,16 @@ module JSONAPI
|
|
454
506
|
end
|
455
507
|
end
|
456
508
|
|
457
|
-
join_manager = JoinManager.new(resource_klass: self,
|
458
|
-
|
459
|
-
|
460
|
-
|
509
|
+
join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
|
510
|
+
source_relationship: relationship,
|
511
|
+
relationships: linkage_relationships,
|
512
|
+
filters: filters)
|
461
513
|
|
462
514
|
paginator = options[:paginator] if source_rids.count == 1
|
463
515
|
|
464
516
|
# Note: We will sort by the source table. Without using unions we can't sort on a polymorphic relationship
|
465
517
|
# in any manner that makes sense
|
466
|
-
records = apply_request_settings_to_records(records:
|
518
|
+
records = apply_request_settings_to_records(records: records_for_source_to_related(options),
|
467
519
|
resource_klass: resource_klass,
|
468
520
|
sort_primary: true,
|
469
521
|
primary_keys: source_ids,
|
@@ -615,14 +667,14 @@ module JSONAPI
|
|
615
667
|
end
|
616
668
|
|
617
669
|
def apply_request_settings_to_records(records:,
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
670
|
+
join_manager: ActiveRelation::JoinManager.new(resource_klass: self),
|
671
|
+
resource_klass: self,
|
672
|
+
filters: {},
|
673
|
+
primary_keys: nil,
|
674
|
+
sort_criteria: nil,
|
675
|
+
sort_primary: nil,
|
676
|
+
paginator: nil,
|
677
|
+
options: {})
|
626
678
|
|
627
679
|
opts = options.dup
|
628
680
|
records = resource_klass.apply_joins(records, join_manager, opts)
|
@@ -777,4 +829,4 @@ module JSONAPI
|
|
777
829
|
end
|
778
830
|
end
|
779
831
|
end
|
780
|
-
end
|
832
|
+
end
|
@@ -70,7 +70,7 @@ module JSONAPI
|
|
70
70
|
def get_related_resources
|
71
71
|
# :nocov:
|
72
72
|
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resources`"\
|
73
|
-
" action. Please use `
|
73
|
+
" action. Please use `index_related_resources` instead."
|
74
74
|
index_related_resources
|
75
75
|
# :nocov:
|
76
76
|
end
|
@@ -0,0 +1,1137 @@
|
|
1
|
+
require 'jsonapi/callbacks'
|
2
|
+
require 'jsonapi/configuration'
|
3
|
+
|
4
|
+
module JSONAPI
|
5
|
+
class BasicResource
|
6
|
+
include Callbacks
|
7
|
+
|
8
|
+
@abstract = true
|
9
|
+
@immutable = true
|
10
|
+
@root = true
|
11
|
+
|
12
|
+
attr_reader :context
|
13
|
+
|
14
|
+
define_jsonapi_resources_callbacks :create,
|
15
|
+
:update,
|
16
|
+
:remove,
|
17
|
+
:save,
|
18
|
+
:create_to_many_link,
|
19
|
+
:replace_to_many_links,
|
20
|
+
:create_to_one_link,
|
21
|
+
:replace_to_one_link,
|
22
|
+
:replace_polymorphic_to_one_link,
|
23
|
+
:remove_to_many_link,
|
24
|
+
:remove_to_one_link,
|
25
|
+
:replace_fields
|
26
|
+
|
27
|
+
def initialize(model, context)
|
28
|
+
@model = model
|
29
|
+
@context = context
|
30
|
+
@reload_needed = false
|
31
|
+
@changing = false
|
32
|
+
@save_needed = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def _model
|
36
|
+
@model
|
37
|
+
end
|
38
|
+
|
39
|
+
def id
|
40
|
+
_model.public_send(self.class._primary_key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def identity
|
44
|
+
JSONAPI::ResourceIdentity.new(self.class, id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def cache_id
|
48
|
+
[id, self.class.hash_cache_field(_model.public_send(self.class._cache_field))]
|
49
|
+
end
|
50
|
+
|
51
|
+
def is_new?
|
52
|
+
id.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def change(callback)
|
56
|
+
completed = false
|
57
|
+
|
58
|
+
if @changing
|
59
|
+
run_callbacks callback do
|
60
|
+
completed = (yield == :completed)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
run_callbacks is_new? ? :create : :update do
|
64
|
+
@changing = true
|
65
|
+
run_callbacks callback do
|
66
|
+
completed = (yield == :completed)
|
67
|
+
end
|
68
|
+
|
69
|
+
completed = (save == :completed) if @save_needed || is_new?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return completed ? :completed : :accepted
|
74
|
+
end
|
75
|
+
|
76
|
+
def remove
|
77
|
+
run_callbacks :remove do
|
78
|
+
_remove
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_to_many_links(relationship_type, relationship_key_values, options = {})
|
83
|
+
change :create_to_many_link do
|
84
|
+
_create_to_many_links(relationship_type, relationship_key_values, options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def replace_to_many_links(relationship_type, relationship_key_values, options = {})
|
89
|
+
change :replace_to_many_links do
|
90
|
+
_replace_to_many_links(relationship_type, relationship_key_values, options)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def replace_to_one_link(relationship_type, relationship_key_value, options = {})
|
95
|
+
change :replace_to_one_link do
|
96
|
+
_replace_to_one_link(relationship_type, relationship_key_value, options)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {})
|
101
|
+
change :replace_polymorphic_to_one_link do
|
102
|
+
_replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def remove_to_many_link(relationship_type, key, options = {})
|
107
|
+
change :remove_to_many_link do
|
108
|
+
_remove_to_many_link(relationship_type, key, options)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def remove_to_one_link(relationship_type, options = {})
|
113
|
+
change :remove_to_one_link do
|
114
|
+
_remove_to_one_link(relationship_type, options)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def replace_fields(field_data)
|
119
|
+
change :replace_fields do
|
120
|
+
_replace_fields(field_data)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Override this on a resource instance to override the fetchable keys
|
125
|
+
def fetchable_fields
|
126
|
+
self.class.fields
|
127
|
+
end
|
128
|
+
|
129
|
+
def model_error_messages
|
130
|
+
_model.errors.messages
|
131
|
+
end
|
132
|
+
|
133
|
+
# Add metadata to validation error objects.
|
134
|
+
#
|
135
|
+
# Suppose `model_error_messages` returned the following error messages
|
136
|
+
# hash:
|
137
|
+
#
|
138
|
+
# {password: ["too_short", "format"]}
|
139
|
+
#
|
140
|
+
# Then to add data to the validation error `validation_error_metadata`
|
141
|
+
# could return:
|
142
|
+
#
|
143
|
+
# {
|
144
|
+
# password: {
|
145
|
+
# "too_short": {"minimum_length" => 6},
|
146
|
+
# "format": {"requirement" => "must contain letters and numbers"}
|
147
|
+
# }
|
148
|
+
# }
|
149
|
+
#
|
150
|
+
# The specified metadata is then be merged into the validation error
|
151
|
+
# object.
|
152
|
+
def validation_error_metadata
|
153
|
+
{}
|
154
|
+
end
|
155
|
+
|
156
|
+
# Override this to return resource level meta data
|
157
|
+
# must return a hash, and if the hash is empty the meta section will not be serialized with the resource
|
158
|
+
# meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
|
159
|
+
# serializer's format_key and format_value methods if desired
|
160
|
+
# the _options hash will contain the serializer and the serialization_options
|
161
|
+
def meta(_options)
|
162
|
+
{}
|
163
|
+
end
|
164
|
+
|
165
|
+
# Override this to return custom links
|
166
|
+
# must return a hash, which will be merged with the default { self: 'self-url' } links hash
|
167
|
+
# links keys will be not be formatted with the key formatter for the serializer by default.
|
168
|
+
# They can however use the serializer's format_key and format_value methods if desired
|
169
|
+
# the _options hash will contain the serializer and the serialization_options
|
170
|
+
def custom_links(_options)
|
171
|
+
{}
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def save
|
177
|
+
run_callbacks :save do
|
178
|
+
_save
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Override this on a resource to return a different result code. Any
|
183
|
+
# value other than :completed will result in operations returning
|
184
|
+
# `:accepted`
|
185
|
+
#
|
186
|
+
# For example to return `:accepted` if your model does not immediately
|
187
|
+
# save resources to the database you could override `_save` as follows:
|
188
|
+
#
|
189
|
+
# ```
|
190
|
+
# def _save
|
191
|
+
# super
|
192
|
+
# return :accepted
|
193
|
+
# end
|
194
|
+
# ```
|
195
|
+
def _save(validation_context = nil)
|
196
|
+
unless @model.valid?(validation_context)
|
197
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
198
|
+
end
|
199
|
+
|
200
|
+
if defined? @model.save
|
201
|
+
saved = @model.save(validate: false)
|
202
|
+
|
203
|
+
unless saved
|
204
|
+
if @model.errors.present?
|
205
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
206
|
+
else
|
207
|
+
fail JSONAPI::Exceptions::SaveFailed.new
|
208
|
+
end
|
209
|
+
end
|
210
|
+
else
|
211
|
+
saved = true
|
212
|
+
end
|
213
|
+
@model.reload if @reload_needed
|
214
|
+
@reload_needed = false
|
215
|
+
|
216
|
+
@save_needed = !saved
|
217
|
+
|
218
|
+
:completed
|
219
|
+
end
|
220
|
+
|
221
|
+
def _remove
|
222
|
+
unless @model.destroy
|
223
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
224
|
+
end
|
225
|
+
:completed
|
226
|
+
|
227
|
+
rescue ActiveRecord::DeleteRestrictionError => e
|
228
|
+
fail JSONAPI::Exceptions::RecordLocked.new(e.message)
|
229
|
+
end
|
230
|
+
|
231
|
+
def reflect_relationship?(relationship, options)
|
232
|
+
return false if !relationship.reflect ||
|
233
|
+
(!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source])
|
234
|
+
|
235
|
+
inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship]
|
236
|
+
if inverse_relationship.nil?
|
237
|
+
warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled."
|
238
|
+
return false
|
239
|
+
end
|
240
|
+
true
|
241
|
+
end
|
242
|
+
|
243
|
+
def _create_to_many_links(relationship_type, relationship_key_values, options)
|
244
|
+
relationship = self.class._relationships[relationship_type]
|
245
|
+
relation_name = relationship.relation_name(context: @context)
|
246
|
+
|
247
|
+
if options[:reflected_source]
|
248
|
+
@model.public_send(relation_name) << options[:reflected_source]._model
|
249
|
+
return :completed
|
250
|
+
end
|
251
|
+
|
252
|
+
# load requested related resources
|
253
|
+
# make sure they all exist (also based on context) and add them to relationship
|
254
|
+
|
255
|
+
related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context)
|
256
|
+
|
257
|
+
if related_resources.count != relationship_key_values.count
|
258
|
+
# todo: obscure id so not to leak info
|
259
|
+
fail JSONAPI::Exceptions::RecordNotFound.new('unspecified')
|
260
|
+
end
|
261
|
+
|
262
|
+
reflect = reflect_relationship?(relationship, options)
|
263
|
+
|
264
|
+
related_resources.each do |related_resource|
|
265
|
+
if reflect
|
266
|
+
if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
|
267
|
+
related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self)
|
268
|
+
else
|
269
|
+
related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self)
|
270
|
+
end
|
271
|
+
@reload_needed = true
|
272
|
+
else
|
273
|
+
unless @model.public_send(relation_name).include?(related_resource._model)
|
274
|
+
@model.public_send(relation_name) << related_resource._model
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
:completed
|
280
|
+
end
|
281
|
+
|
282
|
+
def _replace_to_many_links(relationship_type, relationship_key_values, options)
|
283
|
+
relationship = self.class._relationship(relationship_type)
|
284
|
+
|
285
|
+
reflect = reflect_relationship?(relationship, options)
|
286
|
+
|
287
|
+
if reflect
|
288
|
+
existing_rids = self.class.find_related_fragments([identity], relationship_type, options)
|
289
|
+
|
290
|
+
existing = existing_rids.keys.collect { |rid| rid.id }
|
291
|
+
|
292
|
+
to_delete = existing - (relationship_key_values & existing)
|
293
|
+
to_delete.each do |key|
|
294
|
+
_remove_to_many_link(relationship_type, key, reflected_source: self)
|
295
|
+
end
|
296
|
+
|
297
|
+
to_add = relationship_key_values - (relationship_key_values & existing)
|
298
|
+
_create_to_many_links(relationship_type, to_add, {})
|
299
|
+
|
300
|
+
@reload_needed = true
|
301
|
+
elsif relationship.polymorphic?
|
302
|
+
relationship_key_values.each do |relationship_key_value|
|
303
|
+
relationship_resource_klass = self.class.resource_klass_for(relationship_key_value[:type])
|
304
|
+
ids = relationship_key_value[:ids]
|
305
|
+
|
306
|
+
related_records = relationship_resource_klass
|
307
|
+
.records(options)
|
308
|
+
.where({relationship_resource_klass._primary_key => ids})
|
309
|
+
|
310
|
+
missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
|
311
|
+
|
312
|
+
if missed_ids.present?
|
313
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
|
314
|
+
end
|
315
|
+
|
316
|
+
relation_name = relationship.relation_name(context: @context)
|
317
|
+
@model.send("#{relation_name}") << related_records
|
318
|
+
end
|
319
|
+
|
320
|
+
@reload_needed = true
|
321
|
+
else
|
322
|
+
send("#{relationship.foreign_key}=", relationship_key_values)
|
323
|
+
@save_needed = true
|
324
|
+
end
|
325
|
+
|
326
|
+
:completed
|
327
|
+
end
|
328
|
+
|
329
|
+
def _replace_to_one_link(relationship_type, relationship_key_value, _options)
|
330
|
+
relationship = self.class._relationships[relationship_type]
|
331
|
+
|
332
|
+
send("#{relationship.foreign_key}=", relationship_key_value)
|
333
|
+
@save_needed = true
|
334
|
+
|
335
|
+
:completed
|
336
|
+
end
|
337
|
+
|
338
|
+
def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, _options)
|
339
|
+
relationship = self.class._relationships[relationship_type.to_sym]
|
340
|
+
|
341
|
+
send("#{relationship.foreign_key}=", {type: key_type, id: key_value})
|
342
|
+
@save_needed = true
|
343
|
+
|
344
|
+
:completed
|
345
|
+
end
|
346
|
+
|
347
|
+
def _remove_to_many_link(relationship_type, key, options)
|
348
|
+
relationship = self.class._relationships[relationship_type]
|
349
|
+
|
350
|
+
reflect = reflect_relationship?(relationship, options)
|
351
|
+
|
352
|
+
if reflect
|
353
|
+
|
354
|
+
related_resource = relationship.resource_klass.find_by_key(key, context: @context)
|
355
|
+
|
356
|
+
if related_resource.nil?
|
357
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key)
|
358
|
+
else
|
359
|
+
if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
|
360
|
+
related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self)
|
361
|
+
else
|
362
|
+
related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
@reload_needed = true
|
367
|
+
else
|
368
|
+
@model.public_send(relationship.relation_name(context: @context)).destroy(key)
|
369
|
+
end
|
370
|
+
|
371
|
+
:completed
|
372
|
+
|
373
|
+
rescue ActiveRecord::DeleteRestrictionError => e
|
374
|
+
fail JSONAPI::Exceptions::RecordLocked.new(e.message)
|
375
|
+
rescue ActiveRecord::RecordNotFound
|
376
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key)
|
377
|
+
end
|
378
|
+
|
379
|
+
def _remove_to_one_link(relationship_type, _options)
|
380
|
+
relationship = self.class._relationships[relationship_type]
|
381
|
+
|
382
|
+
send("#{relationship.foreign_key}=", nil)
|
383
|
+
@save_needed = true
|
384
|
+
|
385
|
+
:completed
|
386
|
+
end
|
387
|
+
|
388
|
+
def _replace_fields(field_data)
|
389
|
+
field_data[:attributes].each do |attribute, value|
|
390
|
+
begin
|
391
|
+
send "#{attribute}=", value
|
392
|
+
@save_needed = true
|
393
|
+
rescue ArgumentError
|
394
|
+
# :nocov: Will be thrown if an enum value isn't allowed for an enum. Currently not tested as enums are a rails 4.1 and higher feature
|
395
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value)
|
396
|
+
# :nocov:
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
field_data[:to_one].each do |relationship_type, value|
|
401
|
+
if value.nil?
|
402
|
+
remove_to_one_link(relationship_type)
|
403
|
+
else
|
404
|
+
case value
|
405
|
+
when Hash
|
406
|
+
replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
|
407
|
+
else
|
408
|
+
replace_to_one_link(relationship_type, value)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end if field_data[:to_one]
|
412
|
+
|
413
|
+
field_data[:to_many].each do |relationship_type, values|
|
414
|
+
replace_to_many_links(relationship_type, values)
|
415
|
+
end if field_data[:to_many]
|
416
|
+
|
417
|
+
:completed
|
418
|
+
end
|
419
|
+
|
420
|
+
class << self
|
421
|
+
def inherited(subclass)
|
422
|
+
subclass.abstract(false)
|
423
|
+
subclass.immutable(false)
|
424
|
+
subclass.caching(_caching)
|
425
|
+
subclass.singleton(singleton?, (_singleton_options.dup || {}))
|
426
|
+
subclass.exclude_links(_exclude_links)
|
427
|
+
subclass.paginator(_paginator)
|
428
|
+
subclass._attributes = (_attributes || {}).dup
|
429
|
+
subclass.polymorphic(false)
|
430
|
+
|
431
|
+
subclass._model_hints = (_model_hints || {}).dup
|
432
|
+
|
433
|
+
unless _model_name.empty? || _immutable
|
434
|
+
subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
|
435
|
+
end
|
436
|
+
|
437
|
+
subclass.rebuild_relationships(_relationships || {})
|
438
|
+
|
439
|
+
subclass._allowed_filters = (_allowed_filters || Set.new).dup
|
440
|
+
|
441
|
+
subclass._allowed_sort = _allowed_sort.dup
|
442
|
+
|
443
|
+
type = subclass.name.demodulize.sub(/Resource$/, '').underscore
|
444
|
+
subclass._type = type.pluralize.to_sym
|
445
|
+
|
446
|
+
unless subclass._attributes[:id]
|
447
|
+
subclass.attribute :id, format: :id, readonly: true
|
448
|
+
end
|
449
|
+
|
450
|
+
check_reserved_resource_name(subclass._type, subclass.name)
|
451
|
+
end
|
452
|
+
|
453
|
+
def rebuild_relationships(relationships)
|
454
|
+
original_relationships = relationships.deep_dup
|
455
|
+
|
456
|
+
@_relationships = {}
|
457
|
+
|
458
|
+
if original_relationships.is_a?(Hash)
|
459
|
+
original_relationships.each_value do |relationship|
|
460
|
+
options = relationship.options.dup
|
461
|
+
options[:parent_resource] = self
|
462
|
+
options[:inverse_relationship] = relationship.inverse_relationship
|
463
|
+
_add_relationship(relationship.class, relationship.name, options)
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
def resource_klass_for(type)
|
469
|
+
type = type.underscore
|
470
|
+
type_with_module = type.start_with?(module_path) ? type : module_path + type
|
471
|
+
|
472
|
+
resource_name = _resource_name_from_type(type_with_module)
|
473
|
+
resource = resource_name.safe_constantize if resource_name
|
474
|
+
if resource.nil?
|
475
|
+
fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
|
476
|
+
end
|
477
|
+
resource
|
478
|
+
end
|
479
|
+
|
480
|
+
def resource_klass_for_model(model)
|
481
|
+
resource_klass_for(resource_type_for(model))
|
482
|
+
end
|
483
|
+
|
484
|
+
def _resource_name_from_type(type)
|
485
|
+
"#{type.to_s.underscore.singularize}_resource".camelize
|
486
|
+
end
|
487
|
+
|
488
|
+
def resource_type_for(model)
|
489
|
+
model_name = model.class.to_s.underscore
|
490
|
+
if _model_hints[model_name]
|
491
|
+
_model_hints[model_name]
|
492
|
+
else
|
493
|
+
model_name.rpartition('/').last
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
attr_accessor :_attributes, :_relationships, :_type, :_model_hints
|
498
|
+
attr_writer :_allowed_filters, :_paginator, :_allowed_sort
|
499
|
+
|
500
|
+
def create(context)
|
501
|
+
new(create_model, context)
|
502
|
+
end
|
503
|
+
|
504
|
+
def create_model
|
505
|
+
_model_class.new
|
506
|
+
end
|
507
|
+
|
508
|
+
def routing_options(options)
|
509
|
+
@_routing_resource_options = options
|
510
|
+
end
|
511
|
+
|
512
|
+
def routing_resource_options
|
513
|
+
@_routing_resource_options ||= {}
|
514
|
+
end
|
515
|
+
|
516
|
+
# Methods used in defining a resource class
|
517
|
+
def attributes(*attrs)
|
518
|
+
options = attrs.extract_options!.dup
|
519
|
+
attrs.each do |attr|
|
520
|
+
attribute(attr, options)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def attribute(attribute_name, options = {})
|
525
|
+
attr = attribute_name.to_sym
|
526
|
+
|
527
|
+
check_reserved_attribute_name(attr)
|
528
|
+
|
529
|
+
if (attr == :id) && (options[:format].nil?)
|
530
|
+
ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
|
531
|
+
end
|
532
|
+
|
533
|
+
check_duplicate_attribute_name(attr) if options[:format].nil?
|
534
|
+
|
535
|
+
@_attributes ||= {}
|
536
|
+
@_attributes[attr] = options
|
537
|
+
define_method attr do
|
538
|
+
@model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
|
539
|
+
end unless method_defined?(attr)
|
540
|
+
|
541
|
+
define_method "#{attr}=" do |value|
|
542
|
+
@model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
|
543
|
+
end unless method_defined?("#{attr}=")
|
544
|
+
|
545
|
+
if options.fetch(:sortable, true) && !_has_sort?(attr)
|
546
|
+
sort attr
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def attribute_to_model_field(attribute)
|
551
|
+
field_name = if attribute == :_cache_field
|
552
|
+
_cache_field
|
553
|
+
else
|
554
|
+
# Note: this will allow the returning of model attributes without a corresponding
|
555
|
+
# resource attribute, for example a belongs_to id such as `author_id` or bypassing
|
556
|
+
# the delegate.
|
557
|
+
attr = @_attributes[attribute]
|
558
|
+
attr && attr[:delegate] ? attr[:delegate].to_sym : attribute
|
559
|
+
end
|
560
|
+
if Rails::VERSION::MAJOR >= 5
|
561
|
+
attribute_type = _model_class.attribute_types[field_name.to_s]
|
562
|
+
else
|
563
|
+
attribute_type = _model_class.column_types[field_name.to_s]
|
564
|
+
end
|
565
|
+
{ name: field_name, type: attribute_type}
|
566
|
+
end
|
567
|
+
|
568
|
+
def cast_to_attribute_type(value, type)
|
569
|
+
if Rails::VERSION::MAJOR >= 5
|
570
|
+
return type.cast(value)
|
571
|
+
else
|
572
|
+
return type.type_cast_from_database(value)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
def default_attribute_options
|
577
|
+
{ format: :default }
|
578
|
+
end
|
579
|
+
|
580
|
+
def relationship(*attrs)
|
581
|
+
options = attrs.extract_options!
|
582
|
+
klass = case options[:to]
|
583
|
+
when :one
|
584
|
+
Relationship::ToOne
|
585
|
+
when :many
|
586
|
+
Relationship::ToMany
|
587
|
+
else
|
588
|
+
#:nocov:#
|
589
|
+
fail ArgumentError.new('to: must be either :one or :many')
|
590
|
+
#:nocov:#
|
591
|
+
end
|
592
|
+
_add_relationship(klass, *attrs, options.except(:to))
|
593
|
+
end
|
594
|
+
|
595
|
+
def has_one(*attrs)
|
596
|
+
_add_relationship(Relationship::ToOne, *attrs)
|
597
|
+
end
|
598
|
+
|
599
|
+
def belongs_to(*attrs)
|
600
|
+
ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
|
601
|
+
" using the `belongs_to` class method. We think `has_one`" \
|
602
|
+
" is more appropriate. If you know what you're doing," \
|
603
|
+
" and don't want to see this warning again, override the" \
|
604
|
+
" `belongs_to` class method on your resource."
|
605
|
+
_add_relationship(Relationship::ToOne, *attrs)
|
606
|
+
end
|
607
|
+
|
608
|
+
def has_many(*attrs)
|
609
|
+
_add_relationship(Relationship::ToMany, *attrs)
|
610
|
+
end
|
611
|
+
|
612
|
+
# @model_class is inherited from superclass, and this causes some issues:
|
613
|
+
# ```
|
614
|
+
# CarResource._model_class #=> Vehicle # it should be Car
|
615
|
+
# ```
|
616
|
+
# so in order to invoke the right class from subclasses,
|
617
|
+
# we should call this method to override it.
|
618
|
+
def model_name(model, options = {})
|
619
|
+
@model_class = nil
|
620
|
+
@_model_name = model.to_sym
|
621
|
+
|
622
|
+
model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
|
623
|
+
|
624
|
+
rebuild_relationships(_relationships)
|
625
|
+
end
|
626
|
+
|
627
|
+
def model_hint(model: _model_name, resource: _type)
|
628
|
+
resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
|
629
|
+
|
630
|
+
_model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
|
631
|
+
end
|
632
|
+
|
633
|
+
def singleton(*attrs)
|
634
|
+
@_singleton = (!!attrs[0] == attrs[0]) ? attrs[0] : true
|
635
|
+
@_singleton_options = attrs.extract_options!
|
636
|
+
end
|
637
|
+
|
638
|
+
def _singleton_options
|
639
|
+
@_singleton_options ||= {}
|
640
|
+
end
|
641
|
+
|
642
|
+
def singleton?
|
643
|
+
@_singleton ||= false
|
644
|
+
end
|
645
|
+
|
646
|
+
def filters(*attrs)
|
647
|
+
@_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
|
648
|
+
end
|
649
|
+
|
650
|
+
def filter(attr, *args)
|
651
|
+
@_allowed_filters[attr.to_sym] = args.extract_options!
|
652
|
+
end
|
653
|
+
|
654
|
+
def sort(sorting, options = {})
|
655
|
+
self._allowed_sort[sorting.to_sym] = options
|
656
|
+
end
|
657
|
+
|
658
|
+
def sorts(*args)
|
659
|
+
options = args.extract_options!
|
660
|
+
_allowed_sort.merge!(args.inject({}) { |h, sorting| h[sorting.to_sym] = options.dup; h })
|
661
|
+
end
|
662
|
+
|
663
|
+
def primary_key(key)
|
664
|
+
@_primary_key = key.to_sym
|
665
|
+
end
|
666
|
+
|
667
|
+
def cache_field(field)
|
668
|
+
@_cache_field = field.to_sym
|
669
|
+
end
|
670
|
+
|
671
|
+
# Override in your resource to filter the updatable keys
|
672
|
+
def updatable_fields(_context = nil)
|
673
|
+
_updatable_relationships | _updatable_attributes - [:id]
|
674
|
+
end
|
675
|
+
|
676
|
+
# Override in your resource to filter the creatable keys
|
677
|
+
def creatable_fields(_context = nil)
|
678
|
+
_updatable_relationships | _updatable_attributes
|
679
|
+
end
|
680
|
+
|
681
|
+
# Override in your resource to filter the sortable keys
|
682
|
+
def sortable_fields(_context = nil)
|
683
|
+
_allowed_sort.keys
|
684
|
+
end
|
685
|
+
|
686
|
+
def sortable_field?(key, context = nil)
|
687
|
+
sortable_fields(context).include? key.to_sym
|
688
|
+
end
|
689
|
+
|
690
|
+
def fields
|
691
|
+
_relationships.keys | _attributes.keys
|
692
|
+
end
|
693
|
+
|
694
|
+
def resources_for(records, context)
|
695
|
+
records.collect do |record|
|
696
|
+
resource_for(record, context)
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
def resource_for(model_record, context)
|
701
|
+
resource_klass = self.resource_klass_for_model(model_record)
|
702
|
+
resource_klass.new(model_record, context)
|
703
|
+
end
|
704
|
+
|
705
|
+
def verify_filters(filters, context = nil)
|
706
|
+
verified_filters = {}
|
707
|
+
filters.each do |filter, raw_value|
|
708
|
+
verified_filter = verify_filter(filter, raw_value, context)
|
709
|
+
verified_filters[verified_filter[0]] = verified_filter[1]
|
710
|
+
end
|
711
|
+
verified_filters
|
712
|
+
end
|
713
|
+
|
714
|
+
def is_filter_relationship?(filter)
|
715
|
+
filter == _type || _relationships.include?(filter)
|
716
|
+
end
|
717
|
+
|
718
|
+
def verify_filter(filter, raw, context = nil)
|
719
|
+
filter_values = []
|
720
|
+
if raw.present?
|
721
|
+
begin
|
722
|
+
filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
|
723
|
+
rescue CSV::MalformedCSVError
|
724
|
+
filter_values << raw
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
|
729
|
+
|
730
|
+
if strategy
|
731
|
+
values = call_method_or_proc(strategy, filter_values, context)
|
732
|
+
[filter, values]
|
733
|
+
else
|
734
|
+
if is_filter_relationship?(filter)
|
735
|
+
verify_relationship_filter(filter, filter_values, context)
|
736
|
+
else
|
737
|
+
verify_custom_filter(filter, filter_values, context)
|
738
|
+
end
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
def call_method_or_proc(strategy, *args)
|
743
|
+
if strategy.is_a?(Symbol) || strategy.is_a?(String)
|
744
|
+
send(strategy, *args)
|
745
|
+
else
|
746
|
+
strategy.call(*args)
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
def key_type(key_type)
|
751
|
+
@_resource_key_type = key_type
|
752
|
+
end
|
753
|
+
|
754
|
+
def resource_key_type
|
755
|
+
@_resource_key_type ||= JSONAPI.configuration.resource_key_type
|
756
|
+
end
|
757
|
+
|
758
|
+
# override to all resolution of masked ids to actual ids. Because singleton routes do not specify the id this
|
759
|
+
# will be needed to allow lookup of singleton resources. Alternately singleton resources can override
|
760
|
+
# `verify_key`
|
761
|
+
def singleton_key(context)
|
762
|
+
if @_singleton_options && @_singleton_options[:singleton_key]
|
763
|
+
strategy = @_singleton_options[:singleton_key]
|
764
|
+
case strategy
|
765
|
+
when Proc
|
766
|
+
key = strategy.call(context)
|
767
|
+
when Symbol, String
|
768
|
+
key = send(strategy, context)
|
769
|
+
else
|
770
|
+
raise "singleton_key must be a proc or function name"
|
771
|
+
end
|
772
|
+
end
|
773
|
+
key
|
774
|
+
end
|
775
|
+
|
776
|
+
def verify_key(key, context = nil)
|
777
|
+
key_type = resource_key_type
|
778
|
+
|
779
|
+
case key_type
|
780
|
+
when :integer
|
781
|
+
return if key.nil?
|
782
|
+
Integer(key)
|
783
|
+
when :string
|
784
|
+
return if key.nil?
|
785
|
+
if key.to_s.include?(',')
|
786
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
787
|
+
else
|
788
|
+
key
|
789
|
+
end
|
790
|
+
when :uuid
|
791
|
+
return if key.nil?
|
792
|
+
if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
|
793
|
+
key
|
794
|
+
else
|
795
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
796
|
+
end
|
797
|
+
else
|
798
|
+
key_type.call(key, context)
|
799
|
+
end
|
800
|
+
rescue
|
801
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
802
|
+
end
|
803
|
+
|
804
|
+
# override to allow for key processing and checking
|
805
|
+
def verify_keys(keys, context = nil)
|
806
|
+
return keys.collect do |key|
|
807
|
+
verify_key(key, context)
|
808
|
+
end
|
809
|
+
end
|
810
|
+
|
811
|
+
# Either add a custom :verify lambda or override verify_custom_filter to allow for custom filters
|
812
|
+
def verify_custom_filter(filter, value, _context = nil)
|
813
|
+
[filter, value]
|
814
|
+
end
|
815
|
+
|
816
|
+
# Either add a custom :verify lambda or override verify_relationship_filter to allow for custom
|
817
|
+
# relationship logic, such as uuids, multiple keys or permission checks on keys
|
818
|
+
def verify_relationship_filter(filter, raw, _context = nil)
|
819
|
+
[filter, raw]
|
820
|
+
end
|
821
|
+
|
822
|
+
# quasi private class methods
|
823
|
+
def _attribute_options(attr)
|
824
|
+
default_attribute_options.merge(@_attributes[attr])
|
825
|
+
end
|
826
|
+
|
827
|
+
def _attribute_delegated_name(attr)
|
828
|
+
@_attributes.fetch(attr.to_sym, {}).fetch(:delegate, attr)
|
829
|
+
end
|
830
|
+
|
831
|
+
def _has_attribute?(attr)
|
832
|
+
@_attributes.keys.include?(attr.to_sym)
|
833
|
+
end
|
834
|
+
|
835
|
+
def _updatable_attributes
|
836
|
+
_attributes.map { |key, options| key unless options[:readonly] }.compact
|
837
|
+
end
|
838
|
+
|
839
|
+
def _updatable_relationships
|
840
|
+
@_relationships.map { |key, relationship| key unless relationship.readonly? }.compact
|
841
|
+
end
|
842
|
+
|
843
|
+
def _relationship(type)
|
844
|
+
return nil unless type
|
845
|
+
type = type.to_sym
|
846
|
+
@_relationships[type]
|
847
|
+
end
|
848
|
+
|
849
|
+
def _model_name
|
850
|
+
if _abstract
|
851
|
+
''
|
852
|
+
else
|
853
|
+
return @_model_name.to_s if defined?(@_model_name)
|
854
|
+
class_name = self.name
|
855
|
+
return '' if class_name.nil?
|
856
|
+
@_model_name = class_name.demodulize.sub(/Resource$/, '')
|
857
|
+
@_model_name.to_s
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
def _polymorphic_name
|
862
|
+
if !_polymorphic
|
863
|
+
''
|
864
|
+
else
|
865
|
+
@_polymorphic_name ||= _model_name.to_s.downcase
|
866
|
+
end
|
867
|
+
end
|
868
|
+
|
869
|
+
def _primary_key
|
870
|
+
@_primary_key ||= _default_primary_key
|
871
|
+
end
|
872
|
+
|
873
|
+
def _default_primary_key
|
874
|
+
@_default_primary_key ||=_model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
|
875
|
+
end
|
876
|
+
|
877
|
+
def _cache_field
|
878
|
+
@_cache_field ||= JSONAPI.configuration.default_resource_cache_field
|
879
|
+
end
|
880
|
+
|
881
|
+
def _table_name
|
882
|
+
@_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
|
883
|
+
end
|
884
|
+
|
885
|
+
def _as_parent_key
|
886
|
+
@_as_parent_key ||= "#{_type.to_s.singularize}_id"
|
887
|
+
end
|
888
|
+
|
889
|
+
def _allowed_filters
|
890
|
+
defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
|
891
|
+
end
|
892
|
+
|
893
|
+
def _allowed_sort
|
894
|
+
@_allowed_sort ||= {}
|
895
|
+
end
|
896
|
+
|
897
|
+
def _paginator
|
898
|
+
@_paginator ||= JSONAPI.configuration.default_paginator
|
899
|
+
end
|
900
|
+
|
901
|
+
def paginator(paginator)
|
902
|
+
@_paginator = paginator
|
903
|
+
end
|
904
|
+
|
905
|
+
def _polymorphic
|
906
|
+
@_polymorphic
|
907
|
+
end
|
908
|
+
|
909
|
+
def polymorphic(polymorphic = true)
|
910
|
+
@_polymorphic = polymorphic
|
911
|
+
end
|
912
|
+
|
913
|
+
def _polymorphic_types
|
914
|
+
@poly_hash ||= {}.tap do |hash|
|
915
|
+
ObjectSpace.each_object do |klass|
|
916
|
+
next unless Module === klass
|
917
|
+
if klass < ActiveRecord::Base
|
918
|
+
klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection|
|
919
|
+
(hash[reflection.options[:as]] ||= []) << klass.name.downcase
|
920
|
+
end
|
921
|
+
end
|
922
|
+
end
|
923
|
+
end
|
924
|
+
@poly_hash[_polymorphic_name.to_sym]
|
925
|
+
end
|
926
|
+
|
927
|
+
def _polymorphic_resource_klasses
|
928
|
+
@_polymorphic_resource_klasses ||= _polymorphic_types.collect do |type|
|
929
|
+
resource_klass_for(type)
|
930
|
+
end
|
931
|
+
end
|
932
|
+
|
933
|
+
def root_resource
|
934
|
+
@abstract = true
|
935
|
+
@immutable = true
|
936
|
+
@root = true
|
937
|
+
end
|
938
|
+
|
939
|
+
def root?
|
940
|
+
@root
|
941
|
+
end
|
942
|
+
|
943
|
+
def abstract(val = true)
|
944
|
+
@abstract = val
|
945
|
+
end
|
946
|
+
|
947
|
+
def _abstract
|
948
|
+
@abstract
|
949
|
+
end
|
950
|
+
|
951
|
+
def immutable(val = true)
|
952
|
+
@immutable = val
|
953
|
+
end
|
954
|
+
|
955
|
+
def _immutable
|
956
|
+
@immutable
|
957
|
+
end
|
958
|
+
|
959
|
+
def mutable?
|
960
|
+
!@immutable
|
961
|
+
end
|
962
|
+
|
963
|
+
def exclude_links(exclude)
|
964
|
+
case exclude
|
965
|
+
when :default, "default"
|
966
|
+
@_exclude_links = [:self]
|
967
|
+
when :none, "none"
|
968
|
+
@_exclude_links = []
|
969
|
+
when Array
|
970
|
+
@_exclude_links = exclude.collect {|link| link.to_sym}
|
971
|
+
else
|
972
|
+
fail "Invalid exclude_links"
|
973
|
+
end
|
974
|
+
end
|
975
|
+
|
976
|
+
def _exclude_links
|
977
|
+
@_exclude_links ||= []
|
978
|
+
end
|
979
|
+
|
980
|
+
def exclude_link?(link)
|
981
|
+
_exclude_links.include?(link.to_sym)
|
982
|
+
end
|
983
|
+
|
984
|
+
def caching(val = true)
|
985
|
+
@caching = val
|
986
|
+
end
|
987
|
+
|
988
|
+
def _caching
|
989
|
+
@caching
|
990
|
+
end
|
991
|
+
|
992
|
+
def caching?
|
993
|
+
if @caching.nil?
|
994
|
+
!JSONAPI.configuration.resource_cache.nil? && JSONAPI.configuration.default_caching
|
995
|
+
else
|
996
|
+
@caching && !JSONAPI.configuration.resource_cache.nil?
|
997
|
+
end
|
998
|
+
end
|
999
|
+
|
1000
|
+
def attribute_caching_context(_context)
|
1001
|
+
nil
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
# Generate a hashcode from the value to be used as part of the cache lookup
|
1005
|
+
def hash_cache_field(value)
|
1006
|
+
value.hash
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
def _model_class
|
1010
|
+
return nil if _abstract
|
1011
|
+
|
1012
|
+
return @model_class if @model_class
|
1013
|
+
|
1014
|
+
model_name = _model_name
|
1015
|
+
return nil if model_name.to_s.blank?
|
1016
|
+
|
1017
|
+
@model_class = model_name.to_s.safe_constantize
|
1018
|
+
if @model_class.nil?
|
1019
|
+
warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
@model_class
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
def _allowed_filter?(filter)
|
1026
|
+
!_allowed_filters[filter].nil?
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
def _has_sort?(sorting)
|
1030
|
+
!_allowed_sort[sorting.to_sym].nil?
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
def module_path
|
1034
|
+
if name == 'JSONAPI::Resource'
|
1035
|
+
''
|
1036
|
+
else
|
1037
|
+
name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def default_sort
|
1042
|
+
[{field: 'id', direction: :asc}]
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
def construct_order_options(sort_params)
|
1046
|
+
sort_params ||= default_sort
|
1047
|
+
|
1048
|
+
return {} unless sort_params
|
1049
|
+
|
1050
|
+
sort_params.each_with_object({}) do |sort, order_hash|
|
1051
|
+
field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
|
1052
|
+
order_hash[field] = sort[:direction]
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def _add_relationship(klass, *attrs)
|
1057
|
+
options = attrs.extract_options!
|
1058
|
+
options[:parent_resource] = self
|
1059
|
+
|
1060
|
+
attrs.each do |name|
|
1061
|
+
relationship_name = name.to_sym
|
1062
|
+
check_reserved_relationship_name(relationship_name)
|
1063
|
+
check_duplicate_relationship_name(relationship_name)
|
1064
|
+
|
1065
|
+
define_relationship_methods(relationship_name.to_sym, klass, options)
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
# ResourceBuilder methods
|
1070
|
+
def define_relationship_methods(relationship_name, relationship_klass, options)
|
1071
|
+
relationship = register_relationship(
|
1072
|
+
relationship_name,
|
1073
|
+
relationship_klass.new(relationship_name, options)
|
1074
|
+
)
|
1075
|
+
|
1076
|
+
define_foreign_key_setter(relationship)
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def define_foreign_key_setter(relationship)
|
1080
|
+
if relationship.polymorphic?
|
1081
|
+
define_on_resource "#{relationship.foreign_key}=" do |v|
|
1082
|
+
_model.method("#{relationship.foreign_key}=").call(v[:id])
|
1083
|
+
_model.public_send("#{relationship.polymorphic_type}=", v[:type])
|
1084
|
+
end
|
1085
|
+
else
|
1086
|
+
define_on_resource "#{relationship.foreign_key}=" do |value|
|
1087
|
+
_model.method("#{relationship.foreign_key}=").call(value)
|
1088
|
+
end
|
1089
|
+
end
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def define_on_resource(method_name, &block)
|
1093
|
+
return if method_defined?(method_name)
|
1094
|
+
define_method(method_name, block)
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
def register_relationship(name, relationship_object)
|
1098
|
+
@_relationships[name] = relationship_object
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
private
|
1102
|
+
|
1103
|
+
def check_reserved_resource_name(type, name)
|
1104
|
+
if [:ids, :types, :hrefs, :links].include?(type)
|
1105
|
+
warn "[NAME COLLISION] `#{name}` is a reserved resource name."
|
1106
|
+
return
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
def check_reserved_attribute_name(name)
|
1111
|
+
# Allow :id since it can be used to specify the format. Since it is a method on the base Resource
|
1112
|
+
# an attribute method won't be created for it.
|
1113
|
+
if [:type, :_cache_field, :cache_field].include?(name.to_sym)
|
1114
|
+
warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
|
1115
|
+
end
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
def check_reserved_relationship_name(name)
|
1119
|
+
if [:id, :ids, :type, :types].include?(name.to_sym)
|
1120
|
+
warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
def check_duplicate_relationship_name(name)
|
1125
|
+
if _relationships.include?(name.to_sym)
|
1126
|
+
warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
|
1127
|
+
end
|
1128
|
+
end
|
1129
|
+
|
1130
|
+
def check_duplicate_attribute_name(name)
|
1131
|
+
if _attributes.include?(name.to_sym)
|
1132
|
+
warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
|
1133
|
+
end
|
1134
|
+
end
|
1135
|
+
end
|
1136
|
+
end
|
1137
|
+
end
|