caprese 0.2.0
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 +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
|