caprese 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +12 -0
- data/.travis.yml +18 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/caprese.gemspec +38 -0
- data/lib/caprese/adapter/json_api/error.rb +123 -0
- data/lib/caprese/adapter/json_api/json_pointer.rb +52 -0
- data/lib/caprese/adapter/json_api/jsonapi.rb +49 -0
- data/lib/caprese/adapter/json_api/link.rb +88 -0
- data/lib/caprese/adapter/json_api/meta.rb +37 -0
- data/lib/caprese/adapter/json_api/pagination_links.rb +69 -0
- data/lib/caprese/adapter/json_api/relationship.rb +65 -0
- data/lib/caprese/adapter/json_api/resource_identifier.rb +49 -0
- data/lib/caprese/adapter/json_api.rb +509 -0
- data/lib/caprese/concerns/versioning.rb +69 -0
- data/lib/caprese/controller/concerns/callbacks.rb +60 -0
- data/lib/caprese/controller/concerns/errors.rb +42 -0
- data/lib/caprese/controller/concerns/persistence.rb +327 -0
- data/lib/caprese/controller/concerns/query.rb +209 -0
- data/lib/caprese/controller/concerns/relationships.rb +250 -0
- data/lib/caprese/controller/concerns/rendering.rb +43 -0
- data/lib/caprese/controller/concerns/typing.rb +39 -0
- data/lib/caprese/controller.rb +26 -0
- data/lib/caprese/error.rb +121 -0
- data/lib/caprese/errors.rb +69 -0
- data/lib/caprese/record/errors.rb +82 -0
- data/lib/caprese/record.rb +19 -0
- data/lib/caprese/routing/caprese_resources.rb +27 -0
- data/lib/caprese/serializer/concerns/links.rb +96 -0
- data/lib/caprese/serializer/concerns/lookup.rb +37 -0
- data/lib/caprese/serializer/error_serializer.rb +13 -0
- data/lib/caprese/serializer.rb +13 -0
- data/lib/caprese/version.rb +3 -0
- data/lib/caprese.rb +35 -0
- metadata +273 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'caprese/error'
|
3
|
+
require 'caprese/serializer/error_serializer'
|
4
|
+
|
5
|
+
module Caprese
|
6
|
+
module Errors
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
around_action :enable_caprese_style_errors
|
11
|
+
|
12
|
+
rescue_from Error do |e|
|
13
|
+
output = { json: e }
|
14
|
+
render output.merge(e.header)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fail with a controller action error
|
19
|
+
#
|
20
|
+
# @param [Symbol] field the field (a controller param) that caused the error
|
21
|
+
# @param [Symbol] code the code for the error
|
22
|
+
# @param [Hash] t the interpolation params to be passed into I18n.t
|
23
|
+
def error(field: nil, code: :invalid, t: {})
|
24
|
+
fail Error.new(
|
25
|
+
controller: unversion(params[:controller]),
|
26
|
+
action: params[:action],
|
27
|
+
field: field,
|
28
|
+
code: code,
|
29
|
+
t: t
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Temporarily render model errors as Caprese::Record::Errors instead of ActiveModel::Errors
|
36
|
+
def enable_caprese_style_errors
|
37
|
+
Caprese::Record.caprese_style_errors = true
|
38
|
+
yield
|
39
|
+
Caprese::Record.caprese_style_errors = false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
require 'active_record/associations'
|
2
|
+
require 'active_record/validations'
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Caprese
|
6
|
+
module Persistence
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
# Rescue from instances where required parameters are missing
|
11
|
+
#
|
12
|
+
# @note The only instance this may be called, at least in JSON API settings, is a
|
13
|
+
# missing params['data'] param
|
14
|
+
rescue_from ActionController::ParameterMissing do |e|
|
15
|
+
rescue_with_handler Error.new(
|
16
|
+
field: e.param,
|
17
|
+
code: :blank
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
rescue_from ActiveRecord::RecordInvalid do |e|
|
22
|
+
rescue_with_handler RecordInvalidError.new(e.record)
|
23
|
+
end
|
24
|
+
|
25
|
+
rescue_from ActiveRecord::RecordNotDestroyed do |e|
|
26
|
+
rescue_with_handler ActionForbiddenError.new
|
27
|
+
end
|
28
|
+
|
29
|
+
rescue_from ActiveRecord::DeleteRestrictionError do |e|
|
30
|
+
rescue_with_handler DeleteRestrictedError.new(e.message)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates a new record of whatever type a given controller manages
|
35
|
+
#
|
36
|
+
# @note For this action to succeed, the given controller must define `create_params`
|
37
|
+
# @see #create_params
|
38
|
+
#
|
39
|
+
# 1. Check that type of record to be created matches type that the given controller manages
|
40
|
+
# 2. Build the appropriate attributes/associations for the create action
|
41
|
+
# 3. Build a record with the attributes
|
42
|
+
# 4. Execute after_initialize callbacks
|
43
|
+
# 5. Execute before_create callbacks
|
44
|
+
# 6. Execute before_save callbacks
|
45
|
+
# 7. Create the record by saving it (or fail with RecordInvalid and render errors)
|
46
|
+
# 8. Execute after_create callbacks
|
47
|
+
# 9. Execute after_save callbacks
|
48
|
+
# 10. Return the created resource with 204 Created
|
49
|
+
# @see #rescue_from ActiveRecord::RecordInvalid
|
50
|
+
def create
|
51
|
+
fail_on_type_mismatch(data_params[:type])
|
52
|
+
|
53
|
+
record = queried_record_scope.build
|
54
|
+
assign_record_attributes(record, permitted_params_for(:create), data_params)
|
55
|
+
execute_after_initialize_callbacks(record)
|
56
|
+
|
57
|
+
execute_before_create_callbacks(record)
|
58
|
+
execute_before_save_callbacks(record)
|
59
|
+
|
60
|
+
record.save!
|
61
|
+
|
62
|
+
execute_after_create_callbacks(record)
|
63
|
+
execute_after_save_callbacks(record)
|
64
|
+
|
65
|
+
render(
|
66
|
+
json: record,
|
67
|
+
status: :created,
|
68
|
+
fields: query_params[:fields],
|
69
|
+
include: query_params[:include]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Updates a record of whatever type a given controller manages
|
74
|
+
#
|
75
|
+
# @note For this action to succeed, the given controller must define `update_params`
|
76
|
+
# @see #update_params
|
77
|
+
#
|
78
|
+
# 1. Check that type of record to be updated matches type that the given controller manages
|
79
|
+
# 2. Execute before_update callbacks
|
80
|
+
# 3. Execute before_save callbacks
|
81
|
+
# 4. Update the record (or fail with RecordInvalid and render errors)
|
82
|
+
# 5. Execute after_update callbacks
|
83
|
+
# 6. Execute after_save callbacks
|
84
|
+
# 7. Return the updated resource
|
85
|
+
# @see #rescue_from ActiveRecord::RecordInvalid
|
86
|
+
def update
|
87
|
+
fail_on_type_mismatch(data_params[:type])
|
88
|
+
|
89
|
+
execute_before_update_callbacks(queried_record)
|
90
|
+
execute_before_save_callbacks(queried_record)
|
91
|
+
|
92
|
+
assign_record_attributes(queried_record, permitted_params_for(:update), data_params)
|
93
|
+
queried_record.save!
|
94
|
+
|
95
|
+
execute_after_update_callbacks(queried_record)
|
96
|
+
execute_after_save_callbacks(queried_record)
|
97
|
+
|
98
|
+
render(
|
99
|
+
json: queried_record,
|
100
|
+
fields: query_params[:fields],
|
101
|
+
include: query_params[:include]
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Destroys a record of whatever type a given controller manages
|
106
|
+
#
|
107
|
+
# 1. Execute any before_destroy callbacks, with the record to be destroyed passed in
|
108
|
+
# 2. Destroy the record, ensuring that it checks the model for dependencies before doing so
|
109
|
+
# 3. Execute any after_destroy callbacks, with the destroyed resource passed in
|
110
|
+
# 4. Return 204 No Content if the record was successfully deleted
|
111
|
+
def destroy
|
112
|
+
execute_before_destroy_callbacks(queried_record)
|
113
|
+
queried_record.destroy!
|
114
|
+
execute_after_destroy_callbacks(queried_record)
|
115
|
+
|
116
|
+
head :no_content
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Requires the data param standard to JSON API
|
122
|
+
#
|
123
|
+
# @return [StrongParameters] the strong params in the `data` object param
|
124
|
+
def data_params
|
125
|
+
params.require('data')
|
126
|
+
end
|
127
|
+
|
128
|
+
# An array of symbols stating params that are permitted for a #create action
|
129
|
+
# for a record
|
130
|
+
#
|
131
|
+
# @note Abstract function, must be overridden by every controller
|
132
|
+
#
|
133
|
+
# @return [Array] a list of params permitted to create a record of whatever type
|
134
|
+
# a given controller manages
|
135
|
+
def permitted_create_params
|
136
|
+
fail NotImplementedError
|
137
|
+
end
|
138
|
+
|
139
|
+
# An array of symbols stating params that are permitted for a #update action
|
140
|
+
# for a record
|
141
|
+
#
|
142
|
+
# @note Abstract function, must be overridden by every controller
|
143
|
+
#
|
144
|
+
# @return [Array] a list of params permitted to update a record of whatever type
|
145
|
+
# a given controller manages
|
146
|
+
def permitted_update_params
|
147
|
+
fail NotImplementedError
|
148
|
+
end
|
149
|
+
|
150
|
+
# Gets the permitted params for a given action (create, update)
|
151
|
+
#
|
152
|
+
# @param [Symbol] action the action to get permitted params for
|
153
|
+
# @return [Array] the permitted params for a given action
|
154
|
+
def permitted_params_for(action)
|
155
|
+
send("permitted_#{action}_params")
|
156
|
+
end
|
157
|
+
|
158
|
+
# Gets a set of nested params in an action_params definition
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# create_params => [:body, user: [:name, :email]]
|
162
|
+
# nested_params_for(user, create_params)
|
163
|
+
# => [:name, :email]
|
164
|
+
#
|
165
|
+
# @param [Symbol] key the key of the nested params
|
166
|
+
# @param [Array] params the params to search for the key in
|
167
|
+
# @return [Array,Nil] the nested params for a given key
|
168
|
+
def nested_params_for(key, params)
|
169
|
+
params.detect { |p| p.is_a?(Hash) }.try(:[], key.to_sym)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Flattens an array of the top level keys for a given set of params
|
173
|
+
#
|
174
|
+
# @example
|
175
|
+
# create_params => [:body, user: [:name], post: [:title]]
|
176
|
+
# flattened_keys_for(create_params) => [:body, :user, :post]
|
177
|
+
#
|
178
|
+
# @param [Array] params the params to flatten keys for
|
179
|
+
# @return [Array] the flattened array of keys for the action params
|
180
|
+
def flattened_keys_for(params)
|
181
|
+
params.map do |p|
|
182
|
+
if p.is_a?(Hash)
|
183
|
+
p.keys
|
184
|
+
else
|
185
|
+
p
|
186
|
+
end
|
187
|
+
end.flatten
|
188
|
+
end
|
189
|
+
|
190
|
+
# Builds permitted attributes and relationships into the queried record
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
# params = {
|
194
|
+
# type: 'orders',
|
195
|
+
# attributes: { price: '...', other: '...' },
|
196
|
+
# relationships: {
|
197
|
+
# product: {
|
198
|
+
# data: { token: 'asj38k', type: 'products' }
|
199
|
+
# }
|
200
|
+
# }
|
201
|
+
# }
|
202
|
+
# create_params => [:price]
|
203
|
+
#
|
204
|
+
# assign_record_attributes(record, create_params, params)
|
205
|
+
# => { price: '...', product: Product<@token='asj38k'> }
|
206
|
+
#
|
207
|
+
# @example
|
208
|
+
# params = {
|
209
|
+
# type: 'orders',
|
210
|
+
# attributes: { price: '...', other: '...' },
|
211
|
+
# relationships: {
|
212
|
+
# order_items: {
|
213
|
+
# data: [{
|
214
|
+
# type: 'order_items',
|
215
|
+
# attributes: {
|
216
|
+
# title: 'An order item',
|
217
|
+
# amount: 5.0,
|
218
|
+
# tax: 0.0
|
219
|
+
# }
|
220
|
+
# }]
|
221
|
+
# }
|
222
|
+
# }
|
223
|
+
# }
|
224
|
+
#
|
225
|
+
# create_params => [:price, order_items: [:title, :amount, :tax]]
|
226
|
+
#
|
227
|
+
# assign_record_attributes(record, create_params, params) # => {
|
228
|
+
# price: '...',
|
229
|
+
# order_items: [OrderItem<@token=nil,@title='An order item',@amount=5.0,@tax=0.0>]
|
230
|
+
# }
|
231
|
+
#
|
232
|
+
# @param [ActiveRecord::Base] record the record to build attribute into
|
233
|
+
# @param [Array] permitted_params the permitted params for the action
|
234
|
+
# @param [Parameters] data the data sent to the server to construct and assign to the record
|
235
|
+
def assign_record_attributes(record, permitted_params, data)
|
236
|
+
attributes = data[:attributes].try(:permit, *permitted_params) || {}
|
237
|
+
|
238
|
+
data[:relationships]
|
239
|
+
.try(:slice, *flattened_keys_for(permitted_params))
|
240
|
+
.try(:each) do |relationship_name, relationship_data|
|
241
|
+
attributes[relationship_name] = records_for_relationship(
|
242
|
+
record,
|
243
|
+
nested_params_for(relationship_name, permitted_params),
|
244
|
+
relationship_name,
|
245
|
+
relationship_data
|
246
|
+
)
|
247
|
+
end
|
248
|
+
|
249
|
+
record.assign_attributes(attributes)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Gets all the records for a relationship given a relationship data definition
|
253
|
+
#
|
254
|
+
# @param [ActiveRecord::Base] owner the owner of the relationship
|
255
|
+
# @param [Array] permitted_params the permitted params for the
|
256
|
+
# @param [String] relationship_name the name of the relationship to get records for
|
257
|
+
# @param [Hash,Array<Hash>] relationship_data the resource identifier data to use to find/build records
|
258
|
+
# @return [ActiveRecord::Base,Array<ActiveRecord::Base>] the record(s) for the relationship
|
259
|
+
def records_for_relationship(owner, permitted_params, relationship_name, relationship_data)
|
260
|
+
if relationship_data.is_a?(Array)
|
261
|
+
relationship_data.map do |relationship_data_item|
|
262
|
+
ref = record_for_relationship(owner, relationship_name, relationship_data_item[:data])
|
263
|
+
|
264
|
+
if ref && contains_constructable_data?(relationship_data_item[:data])
|
265
|
+
assign_record_attributes(ref, permitted_params, relationship_data_item[:data])
|
266
|
+
end
|
267
|
+
|
268
|
+
ref
|
269
|
+
end
|
270
|
+
else
|
271
|
+
ref = record_for_relationship(owner, relationship_name, relationship_data[:data])
|
272
|
+
|
273
|
+
if ref && contains_constructable_data?(relationship_data[:data])
|
274
|
+
assign_record_attributes(ref, permitted_params, relationship_data[:data])
|
275
|
+
end
|
276
|
+
|
277
|
+
ref
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Given a resource identifier, finds or builds a resource for a relationship
|
282
|
+
#
|
283
|
+
# @param [ActiveRecord::Base] owner the owner of the relationship record
|
284
|
+
# @param [String] relationship_name the name of the relationship
|
285
|
+
# @param [Hash] resource_identifier the resource identifier for the resource
|
286
|
+
# @return [ActiveRecord::Base] the found or built resource for the relationship
|
287
|
+
def record_for_relationship(owner, relationship_name, resource_identifier)
|
288
|
+
record =
|
289
|
+
if (id = resource_identifier[Caprese.config.resource_primary_key])
|
290
|
+
get_record!(
|
291
|
+
resource_identifier[:type],
|
292
|
+
Caprese.config.resource_primary_key,
|
293
|
+
id
|
294
|
+
)
|
295
|
+
elsif contains_constructable_data?(resource_identifier)
|
296
|
+
record_scope(resource_identifier[:type]).build
|
297
|
+
else
|
298
|
+
owner.errors.add(relationship_name)
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
|
302
|
+
record
|
303
|
+
end
|
304
|
+
|
305
|
+
# Assigns permitted attributes for a record in a relationship, for a given action
|
306
|
+
# like create/update
|
307
|
+
#
|
308
|
+
# @param [ActiveRecord::Base] record the relationship record
|
309
|
+
# @param [String] relationship_name the name of the relationship
|
310
|
+
# @param [Symbol] action the action that is calling this method (create, update)
|
311
|
+
# @param [Hash] resource_identifier the resource identifier
|
312
|
+
def assign_relationship_record_attributes(record, relationship_name, action, attributes)
|
313
|
+
record.assign_attributes(
|
314
|
+
attributes.permit(nested_permitted_params_for(action, relationship_name))
|
315
|
+
)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Indicates whether or not :attributes or :relationships keys are in a resource identifier,
|
319
|
+
# thus allowing us to construct this data into the final record
|
320
|
+
#
|
321
|
+
# @param [Hash] resource_identifier the resource identifier to check for constructable data in
|
322
|
+
# @return [Boolean] whether or not the resource identifier contains constructable data
|
323
|
+
def contains_constructable_data?(resource_identifier)
|
324
|
+
[:attributes, :relationships].any? { |k| resource_identifier.key?(k) }
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'caprese/errors'
|
3
|
+
require 'kaminari'
|
4
|
+
|
5
|
+
module Caprese
|
6
|
+
module Query
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
before_action :execute_before_query_callbacks
|
11
|
+
after_action :execute_after_query_callbacks
|
12
|
+
end
|
13
|
+
|
14
|
+
# Standardizes the index action since it always does the same thing
|
15
|
+
def index
|
16
|
+
render(
|
17
|
+
json: queried_collection,
|
18
|
+
fields: query_params[:fields],
|
19
|
+
include: query_params[:include]
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Standardizes the show action since it always does the same thing
|
24
|
+
def show
|
25
|
+
render(
|
26
|
+
json: queried_record,
|
27
|
+
fields: query_params[:fields],
|
28
|
+
include: query_params[:include]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# The params that affect the query and subsequent response
|
33
|
+
#
|
34
|
+
# @example INCLUDE ASSOCIATED RESOURCES
|
35
|
+
# `GET /api/v1/products?include=merchant`
|
36
|
+
#
|
37
|
+
# @example INCLUDE NESTED ASSOCIATED RESOURCES
|
38
|
+
# `GET /api/v1/products?include=merchant.currency`
|
39
|
+
#
|
40
|
+
# @example FIELDSETS
|
41
|
+
# `GET /api/v1/products?include=merchant&fields[product]=title,description&fields[merchant]=id,name`
|
42
|
+
#
|
43
|
+
# @example SORT
|
44
|
+
# `GET /api/v1/products?sort=updated_at`
|
45
|
+
#
|
46
|
+
# @example SORT DESCENDING
|
47
|
+
# `GET /api/v1/products?sort=-updated_at`
|
48
|
+
#
|
49
|
+
# @example PAGINATION
|
50
|
+
# `GET /api/v1/products?page[number]=1&page[size]=5`
|
51
|
+
#
|
52
|
+
# @example LIMIT AND OFFSET
|
53
|
+
# `GET /api/v1/products?limit=1&offset=1`
|
54
|
+
#
|
55
|
+
# @example LIMIT AND OFFSET (GET LAST)
|
56
|
+
# `GET /api/v1/products?limit=1&offset=-1`
|
57
|
+
#
|
58
|
+
# @example FILTERING
|
59
|
+
# `GET /api/v1/products?filter[venue_id]=10`
|
60
|
+
#
|
61
|
+
# @return [Hash] the params that modify our query
|
62
|
+
def query_params
|
63
|
+
if @query_params.blank?
|
64
|
+
@query_params = params.except(:action, :controller)
|
65
|
+
|
66
|
+
# Sort query by column, ascending or descending
|
67
|
+
@query_params[:sort] = @query_params[:sort].split(',') if @query_params[:sort]
|
68
|
+
|
69
|
+
# Convert fields params into arrays for each resource
|
70
|
+
@query_params[:fields].each do |resource, fields|
|
71
|
+
@query_params[:fields][resource] = fields.split(',')
|
72
|
+
end if @query_params[:fields]
|
73
|
+
end
|
74
|
+
|
75
|
+
@query_params
|
76
|
+
end
|
77
|
+
|
78
|
+
# Gets a collection of type `type`, providing the collection as
|
79
|
+
# a `record scope` by which to query records
|
80
|
+
#
|
81
|
+
# @note We use the term scope, because the collection may be all records of that type,
|
82
|
+
# or the records may be scoped further by overriding this method
|
83
|
+
#
|
84
|
+
# @note If no `type` is provided, the type is assumed to be the controller_record_class
|
85
|
+
#
|
86
|
+
# @param [Symbol] type the type to get a record scope for
|
87
|
+
# @return [Relation] the scope of records of type `type`
|
88
|
+
def record_scope(type = nil)
|
89
|
+
(type && record_class(type) || controller_record_class).all
|
90
|
+
end
|
91
|
+
|
92
|
+
# Gets a record in a scope using a column/value to search by
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# get_record(:orders, :id, '1e0da61f-0229-4035-99a5-3e5c37a221fb')
|
96
|
+
# # => Order.find_by(id: '1e0da61f-0229-4035-99a5-3e5c37a221fb')
|
97
|
+
#
|
98
|
+
# @param [Symbol,Relation] scope the scope to find the record in
|
99
|
+
# if Symbol, call record_scope for that type and use it
|
100
|
+
# if Relation, use it as a scope
|
101
|
+
# @param [Symbol] column the name of the column to find the record by
|
102
|
+
# @param [Value] value the value to match to a column/row value
|
103
|
+
# @return [APIRecord] the record that was found
|
104
|
+
def get_record(scope, column, value)
|
105
|
+
scope = record_scope(scope) unless scope.respond_to?(:find_by)
|
106
|
+
|
107
|
+
scope.find_by(column => value)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Gets a record in a scope using a column/value to search by
|
111
|
+
# @note Fails with error 404 Not Found if the record was not found
|
112
|
+
#
|
113
|
+
# @see get_record
|
114
|
+
def get_record!(scope, column, value)
|
115
|
+
scope = record_scope(scope) unless scope.respond_to?(:find_by!)
|
116
|
+
|
117
|
+
begin
|
118
|
+
scope.find_by!(column => value)
|
119
|
+
rescue ActiveRecord::RecordNotFound => e
|
120
|
+
fail RecordNotFoundError.new(
|
121
|
+
field: column,
|
122
|
+
model: scope.name.underscore,
|
123
|
+
value: value
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Applies query_params[:sort] and query_params[:page] to a given scope
|
129
|
+
#
|
130
|
+
# @param [Relation] scope the scope to apply sorting and pagination to
|
131
|
+
# @return [Relation] the sorted and paginated scope
|
132
|
+
def apply_sorting_pagination_to_scope(scope)
|
133
|
+
if query_params[:sort].try(:any?)
|
134
|
+
ordering = {}
|
135
|
+
query_params[:sort].each do |sort_field|
|
136
|
+
ordering = ordering.merge(
|
137
|
+
if sort_field[0] == '-' # EX: -created_at, sort by created_at descending
|
138
|
+
{ sort_field[1..-1] => :desc }
|
139
|
+
else
|
140
|
+
{ sort_field => :asc }
|
141
|
+
end
|
142
|
+
)
|
143
|
+
end
|
144
|
+
scope = scope.reorder(ordering)
|
145
|
+
end
|
146
|
+
|
147
|
+
if query_params[:offset] || query_params[:limit]
|
148
|
+
offset = query_params[:offset].to_i || 0
|
149
|
+
|
150
|
+
if offset < 0
|
151
|
+
offset = scope.count + offset
|
152
|
+
end
|
153
|
+
|
154
|
+
limit = query_params[:limit] && query_params[:limit].to_i || self.config.default_page_size
|
155
|
+
limit = [limit, self.config.max_page_size].min
|
156
|
+
|
157
|
+
scope.offset(offset).limit(limit)
|
158
|
+
else
|
159
|
+
page_number = query_params[:page].try(:[], :number)
|
160
|
+
page_size = query_params[:page].try(:[], :size).try(:to_i) || self.config.default_page_size
|
161
|
+
page_size = [page_size, self.config.max_page_size].min
|
162
|
+
|
163
|
+
scope.page(page_number).per(page_size)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Gets the scope by which to query controller records, taking into account custom scoping and
|
168
|
+
# the filters provided in the query
|
169
|
+
#
|
170
|
+
# @return [Relation] the record scope of the queried controller
|
171
|
+
def queried_record_scope
|
172
|
+
unless @queried_record_scope
|
173
|
+
scope = record_scope
|
174
|
+
|
175
|
+
if scope.any? && query_params[:filter].try(:any?)
|
176
|
+
if (valid_filters = query_params[:filter].select { |k, _| scope.column_names.include? k }).present?
|
177
|
+
valid_filters.each do |k, v|
|
178
|
+
scope = scope.where(k => v)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
@queried_record_scope = scope
|
184
|
+
end
|
185
|
+
|
186
|
+
@queried_record_scope
|
187
|
+
end
|
188
|
+
|
189
|
+
# Gets the record that was queried, i.e. the record corresponding to the primary key in the
|
190
|
+
# route param (/:id), in the queried_record_scope
|
191
|
+
#
|
192
|
+
# @return [ActiveRecord::Base] the queried record
|
193
|
+
def queried_record
|
194
|
+
@queried_record ||=
|
195
|
+
get_record!(
|
196
|
+
queried_record_scope,
|
197
|
+
column = self.config.resource_primary_key,
|
198
|
+
params[column]
|
199
|
+
)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Gets the collection that was queried, i.e. the sorted & paginated queried_record_scope
|
203
|
+
#
|
204
|
+
# @return [Relation] the queried collection
|
205
|
+
def queried_collection
|
206
|
+
@queried_collection ||= apply_sorting_pagination_to_scope(queried_record_scope)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|