jsonapi-resources 0.9.12 → 0.10.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +34 -11
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi-resources.rb +8 -3
- data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
- data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
- data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
- data/lib/jsonapi/compiled_json.rb +11 -1
- data/lib/jsonapi/configuration.rb +44 -18
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/exceptions.rb +43 -40
- data/lib/jsonapi/formatter.rb +3 -3
- data/lib/jsonapi/include_directives.rb +2 -45
- data/lib/jsonapi/link_builder.rb +87 -80
- data/lib/jsonapi/operation.rb +16 -5
- data/lib/jsonapi/operation_result.rb +74 -16
- data/lib/jsonapi/processor.rb +233 -112
- data/lib/jsonapi/relationship.rb +77 -53
- data/lib/jsonapi/request_parser.rb +378 -423
- data/lib/jsonapi/resource.rb +224 -524
- data/lib/jsonapi/resource_controller_metal.rb +2 -2
- data/lib/jsonapi/resource_fragment.rb +47 -0
- data/lib/jsonapi/resource_id_tree.rb +112 -0
- data/lib/jsonapi/resource_identity.rb +42 -0
- data/lib/jsonapi/resource_serializer.rb +133 -301
- data/lib/jsonapi/resource_set.rb +108 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +100 -88
- data/lib/jsonapi/routing_ext.rb +21 -43
- metadata +29 -45
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -0,0 +1,126 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
module ActiveRelationResourceFinder
|
3
|
+
class JoinTree
|
4
|
+
# Stores relationship paths starting from the resource_klass. This allows consolidation of duplicate paths from
|
5
|
+
# relationships, filters and sorts. This enables the determination of table aliases as they are joined.
|
6
|
+
|
7
|
+
attr_reader :resource_klass, :options, :source_relationship
|
8
|
+
|
9
|
+
def initialize(resource_klass:, options: {}, source_relationship: nil, filters: nil, sort_criteria: nil)
|
10
|
+
@resource_klass = resource_klass
|
11
|
+
@options = options
|
12
|
+
@source_relationship = source_relationship
|
13
|
+
|
14
|
+
@join_relationships = {}
|
15
|
+
|
16
|
+
add_sort_criteria(sort_criteria)
|
17
|
+
add_filters(filters)
|
18
|
+
end
|
19
|
+
|
20
|
+
# A hash of joins that can be used to create the required joins
|
21
|
+
def get_joins
|
22
|
+
walk_relation_node(@join_relationships)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_filters(filters)
|
26
|
+
return if filters.blank?
|
27
|
+
filters.each_key do |filter|
|
28
|
+
# Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
|
29
|
+
next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
|
30
|
+
!resource_klass._allowed_filters[filter].try(:[], :perform_joins)
|
31
|
+
|
32
|
+
add_join(filter)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_sort_criteria(sort_criteria)
|
37
|
+
return if sort_criteria.blank?
|
38
|
+
|
39
|
+
sort_criteria.each do |sort|
|
40
|
+
add_join(sort[:field], :left)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def add_join_relationship(parent_joins, join_name, relation_name, type)
|
47
|
+
parent_joins[join_name] ||= {relation_name: relation_name, relationship: {}, type: type}
|
48
|
+
if parent_joins[join_name][:type] == :left && type == :inner
|
49
|
+
parent_joins[join_name][:type] = :inner
|
50
|
+
end
|
51
|
+
parent_joins[join_name][:relationship]
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_join(path, default_type = :inner)
|
55
|
+
relationships, _field = resource_klass.parse_relationship_path(path)
|
56
|
+
|
57
|
+
current_joins = @join_relationships
|
58
|
+
|
59
|
+
terminated = false
|
60
|
+
|
61
|
+
relationships.each do |relationship|
|
62
|
+
if terminated
|
63
|
+
# ToDo: Relax this, if possible
|
64
|
+
# :nocov:
|
65
|
+
warn "Can not nest joins under polymorphic join"
|
66
|
+
# :nocov:
|
67
|
+
end
|
68
|
+
|
69
|
+
if relationship.polymorphic?
|
70
|
+
relation_names = relationship.polymorphic_relations
|
71
|
+
relation_names.each do |relation_name|
|
72
|
+
join_name = "#{relationship.name}[#{relation_name}]"
|
73
|
+
add_join_relationship(current_joins, join_name, relation_name, :left)
|
74
|
+
end
|
75
|
+
terminated = true
|
76
|
+
else
|
77
|
+
join_name = relationship.name
|
78
|
+
current_joins = add_join_relationship(current_joins, join_name, relationship.relation_name(options), default_type)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create a nested set of hashes from an array of path components. This will be used by the `join` methods.
|
84
|
+
# [post, comments] => { post: { comments: {} }
|
85
|
+
def relation_join_hash(path, path_hash = {})
|
86
|
+
relation = path.shift
|
87
|
+
if relation
|
88
|
+
path_hash[relation] = {}
|
89
|
+
relation_join_hash(path, path_hash[relation])
|
90
|
+
end
|
91
|
+
path_hash
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the paths from shortest to longest, allowing the capture of the table alias for earlier paths. For
|
95
|
+
# example posts, posts.comments and then posts.comments.author joined in that order will alow each
|
96
|
+
# alias to be determined whereas just joining posts.comments.author will only record the author alias.
|
97
|
+
# ToDo: Dependence on this specialized logic should be removed in the future, if possible.
|
98
|
+
def walk_relation_node(node, paths = {}, current_relation_path = [], current_relationship_path = [])
|
99
|
+
node.each do |key, value|
|
100
|
+
if current_relation_path.empty? && source_relationship
|
101
|
+
current_relation_path << source_relationship.relation_name(options)
|
102
|
+
end
|
103
|
+
|
104
|
+
current_relation_path << value[:relation_name].to_s
|
105
|
+
current_relationship_path << key.to_s
|
106
|
+
|
107
|
+
rel_path = current_relationship_path.join('.')
|
108
|
+
paths[rel_path] ||= {
|
109
|
+
alias: nil,
|
110
|
+
join_type: value[:type],
|
111
|
+
relation_join_hash: relation_join_hash(current_relation_path.dup)
|
112
|
+
}
|
113
|
+
|
114
|
+
walk_relation_node(value[:relationship],
|
115
|
+
paths,
|
116
|
+
current_relation_path,
|
117
|
+
current_relationship_path)
|
118
|
+
|
119
|
+
current_relation_path.pop
|
120
|
+
current_relationship_path.pop
|
121
|
+
end
|
122
|
+
paths
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -9,9 +9,12 @@ module JSONAPI
|
|
9
9
|
base.extend ClassMethods
|
10
10
|
base.include Callbacks
|
11
11
|
base.cattr_reader :server_error_callbacks
|
12
|
-
base.define_jsonapi_resources_callbacks :process_operations
|
12
|
+
base.define_jsonapi_resources_callbacks :process_operations,
|
13
|
+
:transaction
|
13
14
|
end
|
14
15
|
|
16
|
+
attr_reader :response_document
|
17
|
+
|
15
18
|
def index
|
16
19
|
process_request
|
17
20
|
end
|
@@ -25,22 +28,18 @@ module JSONAPI
|
|
25
28
|
end
|
26
29
|
|
27
30
|
def create
|
28
|
-
return unless verify_content_type_header
|
29
31
|
process_request
|
30
32
|
end
|
31
33
|
|
32
34
|
def create_relationship
|
33
|
-
return unless verify_content_type_header
|
34
35
|
process_request
|
35
36
|
end
|
36
37
|
|
37
38
|
def update_relationship
|
38
|
-
return unless verify_content_type_header
|
39
39
|
process_request
|
40
40
|
end
|
41
41
|
|
42
42
|
def update
|
43
|
-
return unless verify_content_type_header
|
44
43
|
process_request
|
45
44
|
end
|
46
45
|
|
@@ -52,59 +51,93 @@ module JSONAPI
|
|
52
51
|
process_request
|
53
52
|
end
|
54
53
|
|
55
|
-
def
|
54
|
+
def show_related_resource
|
56
55
|
process_request
|
57
56
|
end
|
58
57
|
|
59
|
-
def
|
58
|
+
def index_related_resources
|
60
59
|
process_request
|
61
60
|
end
|
62
61
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
62
|
+
def get_related_resource
|
63
|
+
# :nocov:
|
64
|
+
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resource`"\
|
65
|
+
" action. Please use `show_related_resource` instead."
|
66
|
+
show_related_resource
|
67
|
+
# :nocov:
|
68
|
+
end
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
77
|
-
results = process_operations(operations)
|
78
|
-
render_results(results)
|
79
|
-
end
|
80
|
-
rescue => e
|
81
|
-
handle_exceptions(e)
|
70
|
+
def get_related_resources
|
71
|
+
# :nocov:
|
72
|
+
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resources`"\
|
73
|
+
" action. Please use `index_related_resource` instead."
|
74
|
+
index_related_resources
|
75
|
+
# :nocov:
|
82
76
|
end
|
83
77
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
78
|
+
def process_request
|
79
|
+
@response_document = create_response_document
|
80
|
+
|
81
|
+
unless verify_content_type_header && verify_accept_header
|
82
|
+
render_response_document
|
83
|
+
return
|
87
84
|
end
|
88
|
-
end
|
89
85
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
86
|
+
request_parser = JSONAPI::RequestParser.new(
|
87
|
+
params,
|
88
|
+
context: context,
|
89
|
+
key_formatter: key_formatter,
|
90
|
+
server_error_callbacks: (self.class.server_error_callbacks || []))
|
91
|
+
|
92
|
+
transactional = request_parser.transactional?
|
93
|
+
|
94
|
+
begin
|
95
|
+
process_operations(transactional) do
|
96
|
+
run_callbacks :process_operations do
|
97
|
+
request_parser.each(response_document) do |op|
|
98
|
+
op.options[:serializer] = resource_serializer_klass.new(
|
99
|
+
op.resource_klass,
|
100
|
+
include_directives: op.options[:include_directives],
|
101
|
+
fields: op.options[:fields],
|
102
|
+
base_url: base_url,
|
103
|
+
key_formatter: key_formatter,
|
104
|
+
route_formatter: route_formatter,
|
105
|
+
serialization_options: serialization_options
|
106
|
+
)
|
107
|
+
op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?
|
108
|
+
|
109
|
+
process_operation(op)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
if response_document.has_errors?
|
113
|
+
raise ActiveRecord::Rollback
|
114
|
+
end
|
94
115
|
end
|
95
|
-
|
116
|
+
rescue => e
|
117
|
+
handle_exceptions(e)
|
118
|
+
end
|
119
|
+
render_response_document
|
96
120
|
end
|
97
121
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
122
|
+
def process_operations(transactional)
|
123
|
+
if transactional
|
124
|
+
run_callbacks :transaction do
|
125
|
+
ActiveRecord::Base.transaction do
|
126
|
+
yield
|
127
|
+
end
|
128
|
+
end
|
129
|
+
else
|
130
|
+
begin
|
131
|
+
yield
|
132
|
+
rescue ActiveRecord::Rollback
|
133
|
+
# Can't rollback without transaction, so just ignore it
|
134
|
+
end
|
135
|
+
end
|
102
136
|
end
|
103
137
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
107
|
-
server_error_callbacks: @request.server_error_callbacks)
|
138
|
+
def process_operation(operation)
|
139
|
+
result = operation.process
|
140
|
+
response_document.add_result(result, operation)
|
108
141
|
end
|
109
142
|
|
110
143
|
private
|
@@ -117,20 +150,6 @@ module JSONAPI
|
|
117
150
|
@resource_serializer_klass ||= JSONAPI::ResourceSerializer
|
118
151
|
end
|
119
152
|
|
120
|
-
def resource_serializer
|
121
|
-
@resource_serializer ||= resource_serializer_klass.new(
|
122
|
-
resource_klass,
|
123
|
-
include_directives: @request ? @request.include_directives : nil,
|
124
|
-
fields: @request ? @request.fields : {},
|
125
|
-
base_url: base_url,
|
126
|
-
key_formatter: key_formatter,
|
127
|
-
route_formatter: route_formatter,
|
128
|
-
serialization_options: serialization_options,
|
129
|
-
controller: self
|
130
|
-
)
|
131
|
-
@resource_serializer
|
132
|
-
end
|
133
|
-
|
134
153
|
def base_url
|
135
154
|
@base_url ||= request.protocol + request.host_with_port
|
136
155
|
end
|
@@ -140,8 +159,10 @@ module JSONAPI
|
|
140
159
|
end
|
141
160
|
|
142
161
|
def verify_content_type_header
|
143
|
-
|
144
|
-
|
162
|
+
if ['create', 'create_relationship', 'update_relationship', 'update'].include?(params[:action])
|
163
|
+
unless request.content_type == JSONAPI::MEDIA_TYPE
|
164
|
+
fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
|
165
|
+
end
|
145
166
|
end
|
146
167
|
true
|
147
168
|
rescue => e
|
@@ -162,13 +183,12 @@ module JSONAPI
|
|
162
183
|
def valid_accept_media_type?
|
163
184
|
media_types = media_types_for('Accept')
|
164
185
|
|
165
|
-
media_types.blank? ||
|
166
|
-
|
167
|
-
|
168
|
-
end
|
186
|
+
media_types.blank? || media_types.any? do |media_type|
|
187
|
+
(media_type == JSONAPI::MEDIA_TYPE || media_type.start_with?(ALL_MEDIA_TYPES))
|
188
|
+
end
|
169
189
|
end
|
170
190
|
|
171
|
-
|
191
|
+
def media_types_for(header)
|
172
192
|
(request.headers[header] || '')
|
173
193
|
.scan(MEDIA_TYPE_MATCHER)
|
174
194
|
.to_a
|
@@ -203,57 +223,43 @@ module JSONAPI
|
|
203
223
|
end
|
204
224
|
|
205
225
|
def base_meta
|
206
|
-
|
207
|
-
base_response_meta
|
208
|
-
else
|
209
|
-
base_response_meta.merge(warnings: @request.warnings)
|
210
|
-
end
|
226
|
+
base_response_meta
|
211
227
|
end
|
212
228
|
|
213
229
|
def base_response_links
|
214
230
|
{}
|
215
231
|
end
|
216
232
|
|
217
|
-
def
|
218
|
-
|
219
|
-
result = JSONAPI::ErrorsOperationResult.new(errors[0].status, errors)
|
220
|
-
operation_results.add_result(result)
|
221
|
-
|
222
|
-
render_results(operation_results)
|
223
|
-
end
|
224
|
-
|
225
|
-
def render_results(operation_results)
|
226
|
-
response_doc = create_response_document(operation_results)
|
227
|
-
content = response_doc.contents
|
233
|
+
def render_response_document
|
234
|
+
content = response_document.contents
|
228
235
|
|
229
236
|
render_options = {}
|
230
|
-
if
|
237
|
+
if response_document.has_errors?
|
231
238
|
render_options[:json] = content
|
232
239
|
else
|
233
|
-
#
|
240
|
+
# Bypassing ActiveSupport allows us to use CompiledJson objects for cached response fragments
|
234
241
|
render_options[:body] = JSON.generate(content)
|
235
|
-
end
|
236
242
|
|
237
|
-
|
238
|
-
|
239
|
-
|
243
|
+
if (response_document.status == 201 && content[:data].class != Array) &&
|
244
|
+
content['data'] && content['data']['links'] && content['data']['links']['self']
|
245
|
+
render_options[:location] = content['data']['links']['self']
|
246
|
+
end
|
247
|
+
end
|
240
248
|
|
241
249
|
# For whatever reason, `render` ignores :status and :content_type when :body is set.
|
242
250
|
# But, we can just set those values directly in the Response object instead.
|
243
|
-
response.status =
|
251
|
+
response.status = response_document.status
|
244
252
|
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
245
253
|
|
246
254
|
render(render_options)
|
247
255
|
end
|
248
256
|
|
249
|
-
def create_response_document
|
257
|
+
def create_response_document
|
250
258
|
JSONAPI::ResponseDocument.new(
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
base_links: base_response_links,
|
256
|
-
request: @request
|
259
|
+
key_formatter: key_formatter,
|
260
|
+
base_meta: base_meta,
|
261
|
+
base_links: base_response_links,
|
262
|
+
request: request
|
257
263
|
)
|
258
264
|
end
|
259
265
|
|
@@ -261,21 +267,30 @@ module JSONAPI
|
|
261
267
|
# Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
|
262
268
|
def handle_exceptions(e)
|
263
269
|
case e
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
fail e
|
270
|
+
when JSONAPI::Exceptions::Error
|
271
|
+
errors = e.errors
|
272
|
+
when ActionController::ParameterMissing
|
273
|
+
errors = JSONAPI::Exceptions::ParameterMissing.new(e.param).errors
|
269
274
|
else
|
270
|
-
|
271
|
-
|
272
|
-
|
275
|
+
if JSONAPI.configuration.exception_class_whitelisted?(e)
|
276
|
+
raise e
|
277
|
+
else
|
278
|
+
if self.class.server_error_callbacks
|
279
|
+
self.class.server_error_callbacks.each { |callback|
|
280
|
+
safe_run_callback(callback, e)
|
281
|
+
}
|
282
|
+
end
|
273
283
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
284
|
+
# Store exception for other middlewares
|
285
|
+
request.env['action_dispatch.exception'] ||= e
|
286
|
+
|
287
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
288
|
+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
|
289
|
+
errors = internal_server_error.errors
|
290
|
+
end
|
278
291
|
end
|
292
|
+
|
293
|
+
response_document.add_result(JSONAPI::ErrorsOperationResult.new(errors[0].status, errors), nil)
|
279
294
|
end
|
280
295
|
|
281
296
|
def safe_run_callback(callback, error)
|
@@ -284,7 +299,7 @@ module JSONAPI
|
|
284
299
|
rescue => e
|
285
300
|
Rails.logger.error { "Error in error handling callback: #{e.message} #{e.backtrace.join("\n")}" }
|
286
301
|
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
287
|
-
|
302
|
+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
|
288
303
|
end
|
289
304
|
end
|
290
305
|
|