model-api 0.8.7 → 0.8.8
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/model-api/api_context.rb +444 -0
- data/lib/model-api/base_controller.rb +46 -444
- data/model-api.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff188f893e1fb773d384adfa95ff774a4891eeb4
|
4
|
+
data.tar.gz: cc823f6fb228c8d0a66efcfc0997cd3b7623feba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae2c38e837259c246204d3d350fa771b68a132de7c15f9a88c8a3247fdc69d77620c5756f793e0b75716849a8c8ebab54ca8d7a17a3dd291d4fa6aa555d46ac2
|
7
|
+
data.tar.gz: 225892b57a109cc7fc71657fb6fba1c9a9cedca877dfa01b2222498fa36476e34a4e93afd33cc4c9e6d7dbacd383fa1bda571f14e2d920948b1c2f8b17563d46
|
@@ -0,0 +1,444 @@
|
|
1
|
+
module ModelApi
|
2
|
+
class ApiContext
|
3
|
+
def initialize(context_parent)
|
4
|
+
@context_parent = context_parent
|
5
|
+
end
|
6
|
+
|
7
|
+
def model_class
|
8
|
+
return @model_class if instance_variable_defined?(:@model_class)
|
9
|
+
@model_class = @context_parent.send(:model_class)
|
10
|
+
end
|
11
|
+
|
12
|
+
def prepare_options(opts)
|
13
|
+
return opts if opts[:options_initialized]
|
14
|
+
if @context_parent.respond_to?(:prepare_options, true)
|
15
|
+
return @context_parent.send(:prepare_options, opts)
|
16
|
+
end
|
17
|
+
opts
|
18
|
+
end
|
19
|
+
|
20
|
+
def api_query(klass, opts = {})
|
21
|
+
opts = prepare_options(opts)
|
22
|
+
model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(klass)
|
23
|
+
unless klass < ActiveRecord::Base
|
24
|
+
fail 'Expected model class to be an ActiveRecord::Base subclass'
|
25
|
+
end
|
26
|
+
query = ModelApi::Utils.invoke_callback(model_metadata[:base_query], opts) || klass.all
|
27
|
+
if (deleted_col = klass.columns_hash['deleted']).present?
|
28
|
+
case deleted_col.type
|
29
|
+
when :boolean
|
30
|
+
query = query.where(deleted: false)
|
31
|
+
when :integer, :decimal
|
32
|
+
query = query.where(deleted: 0)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
apply_context(query, opts)
|
36
|
+
end
|
37
|
+
|
38
|
+
def common_object_query(id_attribute, id_value, opts = {})
|
39
|
+
klass = opts[:model_class] || model_class
|
40
|
+
coll_query = apply_context(api_query(klass, opts), opts)
|
41
|
+
query = coll_query.where(id_attribute => id_value)
|
42
|
+
if !opts[:admin_user]
|
43
|
+
unless opts.include?(:user_filter) && !opts[:user_filter] && opts[:user]
|
44
|
+
query = user_query(query, opts[:user], opts.merge(model_class: klass))
|
45
|
+
end
|
46
|
+
elsif id_attribute != :id && !id_attribute.to_s.ends_with?('.id') &&
|
47
|
+
klass.column_names.include?('id') && !query.exists?
|
48
|
+
# Admins can optionally use record ID's if the ID field happens to be something else
|
49
|
+
query = coll_query.where(id: id_value)
|
50
|
+
end
|
51
|
+
unless (not_found_error = opts[:not_found_error]).blank? || query.exists?
|
52
|
+
not_found_error = not_found_error.call(params[:id]) if not_found_error.respond_to?(:call)
|
53
|
+
if not_found_error == true
|
54
|
+
not_found_error = "#{klass.model_name.human} '#{id_value}' not found."
|
55
|
+
end
|
56
|
+
fail ModelApi::NotFoundException.new(opts[:id_param] || id_attribute, not_found_error.to_s)
|
57
|
+
end
|
58
|
+
query
|
59
|
+
end
|
60
|
+
|
61
|
+
def user_query(query, user, opts = {})
|
62
|
+
klass = opts[:model_class] || query.klass
|
63
|
+
user_id_col = opts[:user_id_column] || :user_id
|
64
|
+
user_assoc = opts[:user_association] || :user
|
65
|
+
user_id = user.try(opts[:user_id_attribute] || :id)
|
66
|
+
if klass.columns_hash.include?(user_id_col.to_s)
|
67
|
+
query = query.where(user_id_col => user_id)
|
68
|
+
elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
|
69
|
+
[:belongs_to, :has_one].include?(assoc.macro)
|
70
|
+
query = query.joins(user_assoc).where(
|
71
|
+
"#{assoc.klass.table_name}.#{assoc.klass.primary_key}" => user_id)
|
72
|
+
elsif opts[:user_filter]
|
73
|
+
fail "Unable to filter results by user; no '#{user_id_col}' column or " \
|
74
|
+
"'#{user_assoc}' association found!"
|
75
|
+
end
|
76
|
+
query
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_read_operation(obj, operation, opts = {})
|
80
|
+
opts = prepare_options(opts)
|
81
|
+
status, errors = ModelApi::Utils.validate_operation(obj, operation,
|
82
|
+
opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
|
83
|
+
return true if status.nil? && errors.nil?
|
84
|
+
if errors.nil? && (status.is_a?(Array) || status.present?)
|
85
|
+
return true if (errors = status).blank?
|
86
|
+
status = :bad_request
|
87
|
+
end
|
88
|
+
return true unless errors.present?
|
89
|
+
errors = [errors] unless errors.is_a?(Array)
|
90
|
+
simple_error(status, errors, opts)
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_updated_object(obj_or_class, operation, request_body, opts = {})
|
95
|
+
opts = prepare_options(opts.symbolize_keys)
|
96
|
+
opts[:operation] = operation
|
97
|
+
if obj_or_class.is_a?(Class)
|
98
|
+
klass = class_or_sti_subclass(obj_or_class, request_body, operation, opts)
|
99
|
+
obj = nil
|
100
|
+
elsif obj_or_class.is_a?(ActiveRecord::Base)
|
101
|
+
obj = obj_or_class
|
102
|
+
klass = obj.class
|
103
|
+
elsif obj_or_class.is_a?(ActiveRecord::Relation)
|
104
|
+
klass = obj_or_class.klass
|
105
|
+
obj = obj_or_class.first
|
106
|
+
end
|
107
|
+
opts[:api_attr_metadata] = ModelApi::Utils.filtered_attrs(klass, operation, opts)
|
108
|
+
opts[:api_model_metadata] = model_metadata = ModelApi::Utils.model_metadata(klass)
|
109
|
+
opts[:ignored_fields] = []
|
110
|
+
return [nil, opts.merge(bad_payload: true)] if request_body.nil?
|
111
|
+
obj = klass.new if obj.nil?
|
112
|
+
verify_update_request_body(request_body, opts[:format], opts)
|
113
|
+
root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
|
114
|
+
request_obj = opts[:request_obj] = object_from_req_body(root_elem, request_body,
|
115
|
+
opts[:format])
|
116
|
+
opts[:request_hash] = ModelApi::Utils.internal_value(request_obj).deep_symbolize_keys
|
117
|
+
ModelApi::Utils.apply_updates(obj, request_obj, operation, opts)
|
118
|
+
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
|
119
|
+
[obj, opts]
|
120
|
+
end
|
121
|
+
|
122
|
+
def filter_collection(collection, filter_params, opts = {})
|
123
|
+
return [collection, {}] if filter_params.blank? # Don't filter if no filter params
|
124
|
+
klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
|
125
|
+
assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
|
126
|
+
result_filters = {}
|
127
|
+
metadata.values.each do |attr_metadata|
|
128
|
+
collection = apply_filter_param(attr_metadata, collection,
|
129
|
+
opts.merge(attr_values: attr_values, result_filters: result_filters, class: klass))
|
130
|
+
end
|
131
|
+
assoc_values.each do |assoc, assoc_filter_params|
|
132
|
+
ar_assoc = klass.reflect_on_association(assoc)
|
133
|
+
next unless ar_assoc.present?
|
134
|
+
collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
|
135
|
+
collection, assoc_result_filters = filter_collection(collection, assoc_filter_params,
|
136
|
+
opts.merge(class: ar_assoc.klass, filter_table: ar_assoc.table_name))
|
137
|
+
result_filters[assoc] = assoc_result_filters if assoc_result_filters.present?
|
138
|
+
end
|
139
|
+
[collection, result_filters]
|
140
|
+
end
|
141
|
+
|
142
|
+
def process_filter_params(filter_params, klass, opts = {})
|
143
|
+
assoc_values = {}
|
144
|
+
filter_metadata = {}
|
145
|
+
attr_values = {}
|
146
|
+
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :filter, opts)
|
147
|
+
filter_params.each do |attr, value|
|
148
|
+
attr = attr.to_s
|
149
|
+
if attr.length > 1 && ['>', '<', '!', '='].include?(attr[-1])
|
150
|
+
value = "#{attr[-1]}=#{value}" # Effectively allows >= / <= / != / == in query string
|
151
|
+
attr = attr[0..-2].strip
|
152
|
+
end
|
153
|
+
if attr.include?('.')
|
154
|
+
process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
155
|
+
else
|
156
|
+
process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
[assoc_values, filter_metadata, attr_values]
|
160
|
+
end
|
161
|
+
|
162
|
+
# rubocop:disable Metrics/ParameterLists
|
163
|
+
def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
164
|
+
attr_elems = attr.split('.')
|
165
|
+
assoc_name = attr_elems[0].strip.to_sym
|
166
|
+
assoc_metadata = metadata[assoc_name] ||
|
167
|
+
metadata[ModelApi::Utils.ext_query_attr(assoc_name, opts)] || {}
|
168
|
+
key = assoc_metadata[:key]
|
169
|
+
return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:filter], opts)
|
170
|
+
assoc_filter_params = (assoc_values[key] ||= {})
|
171
|
+
assoc_filter_params[attr_elems[1..-1].join('.')] = value
|
172
|
+
end
|
173
|
+
|
174
|
+
def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
175
|
+
attr = attr.strip.to_sym
|
176
|
+
attr_metadata = metadata[attr] ||
|
177
|
+
metadata[ModelApi::Utils.ext_query_attr(attr, opts)] || {}
|
178
|
+
key = attr_metadata[:key]
|
179
|
+
return unless key.present? && ModelApi::Utils.eval_bool(attr_metadata[:filter], opts)
|
180
|
+
filter_metadata[key] = attr_metadata
|
181
|
+
attr_values[key] = value
|
182
|
+
end
|
183
|
+
|
184
|
+
# rubocop:enable Metrics/ParameterLists
|
185
|
+
|
186
|
+
def apply_filter_param(attr_metadata, collection, opts = {})
|
187
|
+
raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
|
188
|
+
filter_table = opts[:filter_table]
|
189
|
+
klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
|
190
|
+
if raw_value.is_a?(Hash) && raw_value.include?('0')
|
191
|
+
operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
|
192
|
+
opts)
|
193
|
+
else
|
194
|
+
operator_value_pairs = filter_process_param(raw_value, attr_metadata, opts)
|
195
|
+
end
|
196
|
+
if (column = resolve_key_to_column(klass, attr_metadata)).present?
|
197
|
+
operator_value_pairs.each do |operator, value|
|
198
|
+
if operator == '=' && filter_table.blank?
|
199
|
+
collection = collection.where(column => value)
|
200
|
+
else
|
201
|
+
table_name = (filter_table || klass.table_name).to_s.delete('`')
|
202
|
+
column = column.to_s.delete('`')
|
203
|
+
if value.is_a?(Array)
|
204
|
+
operator = 'IN'
|
205
|
+
value = value.map { |_v| format_value_for_query(column, value, klass) }
|
206
|
+
value = "(#{value.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')})"
|
207
|
+
else
|
208
|
+
value = "'#{value.gsub("'", "''")}'"
|
209
|
+
end
|
210
|
+
collection = collection.where("`#{table_name}`.`#{column}` #{operator} #{value}")
|
211
|
+
end
|
212
|
+
end
|
213
|
+
elsif (key = attr_metadata[:key]).present?
|
214
|
+
opts[:result_filters][key] = operator_value_pairs if opts.include?(:result_filters)
|
215
|
+
end
|
216
|
+
collection
|
217
|
+
end
|
218
|
+
|
219
|
+
def sort_collection(collection, sort_params, opts = {})
|
220
|
+
return [collection, {}] if sort_params.blank? # Don't filter if no filter params
|
221
|
+
klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
|
222
|
+
assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
|
223
|
+
opts.merge(result_sorts: result_sorts))
|
224
|
+
sort_table = opts[:sort_table]
|
225
|
+
sort_table = sort_table.to_s.delete('`') if sort_table.present?
|
226
|
+
attr_sorts.each do |key, sort_order|
|
227
|
+
if sort_table.present?
|
228
|
+
collection = collection.order("`#{sort_table}`.`#{key.to_s.delete('`')}` " \
|
229
|
+
"#{sort_order.to_s.upcase}")
|
230
|
+
else
|
231
|
+
collection = collection.order(key => sort_order)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
assoc_sorts.each do |assoc, assoc_sort_params|
|
235
|
+
ar_assoc = klass.reflect_on_association(assoc)
|
236
|
+
next unless ar_assoc.present?
|
237
|
+
collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
|
238
|
+
collection, assoc_result_sorts = sort_collection(collection, assoc_sort_params,
|
239
|
+
opts.merge(class: ar_assoc.klass, sort_table: ar_assoc.table_name))
|
240
|
+
result_sorts[assoc] = assoc_result_sorts if assoc_result_sorts.present?
|
241
|
+
end
|
242
|
+
[collection, result_sorts]
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
def apply_context(query, opts = {})
|
248
|
+
context = opts[:context]
|
249
|
+
return query if context.nil?
|
250
|
+
if context.respond_to?(:call)
|
251
|
+
query = context.send(*([:call, query, opts][0..context.parameters.size]))
|
252
|
+
elsif context.is_a?(Hash)
|
253
|
+
context.each { |attr, value| query = query.where(attr => value) }
|
254
|
+
end
|
255
|
+
query
|
256
|
+
end
|
257
|
+
|
258
|
+
def process_sort_params(sort_params, klass, opts)
|
259
|
+
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
|
260
|
+
assoc_sorts = {}
|
261
|
+
attr_sorts = {}
|
262
|
+
result_sorts = {}
|
263
|
+
sort_params.each do |attr, sort_order|
|
264
|
+
if attr.include?('.')
|
265
|
+
process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
266
|
+
else
|
267
|
+
attr = attr.strip.to_sym
|
268
|
+
attr_metadata = metadata[attr] || {}
|
269
|
+
next unless ModelApi::Utils.eval_bool(attr_metadata[:sort], opts)
|
270
|
+
sort_order = sort_order.to_sym
|
271
|
+
sort_order = :default unless [:asc, :desc].include?(sort_order)
|
272
|
+
if sort_order == :default
|
273
|
+
sort_order = (attr_metadata[:default_sort_order] || :asc).to_sym
|
274
|
+
sort_order = :asc unless [:asc, :desc].include?(sort_order)
|
275
|
+
end
|
276
|
+
if (column = resolve_key_to_column(klass, attr_metadata)).present?
|
277
|
+
attr_sorts[column] = sort_order
|
278
|
+
elsif (key = attr_metadata[:key]).present?
|
279
|
+
result_sorts[key] = sort_order
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
[assoc_sorts, attr_sorts, result_sorts]
|
284
|
+
end
|
285
|
+
|
286
|
+
# Intentionally disabling parameter list length check for private / internal method
|
287
|
+
# rubocop:disable Metrics/ParameterLists
|
288
|
+
def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
289
|
+
attr_elems = attr.split('.')
|
290
|
+
assoc_name = attr_elems[0].strip.to_sym
|
291
|
+
assoc_metadata = metadata[assoc_name] || {}
|
292
|
+
key = assoc_metadata[:key]
|
293
|
+
return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:sort], opts)
|
294
|
+
assoc_sort_params = (assoc_sorts[key] ||= {})
|
295
|
+
assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
|
296
|
+
end
|
297
|
+
|
298
|
+
# rubocop:enable Metrics/ParameterLists
|
299
|
+
|
300
|
+
def filter_process_param(raw_value, attr_metadata, opts)
|
301
|
+
raw_value = raw_value.to_s.strip
|
302
|
+
array = nil
|
303
|
+
if raw_value.starts_with?('[') && raw_value.ends_with?(']')
|
304
|
+
array = JSON.parse(raw_value) rescue nil
|
305
|
+
array = array.is_a?(Array) ? array.map(&:to_s) : nil
|
306
|
+
end
|
307
|
+
if array.nil?
|
308
|
+
if attr_metadata.include?(:filter_delimiter)
|
309
|
+
delimiter = attr_metadata[:filter_delimiter]
|
310
|
+
else
|
311
|
+
delimiter = ','
|
312
|
+
end
|
313
|
+
array = raw_value.split(delimiter) if raw_value.include?(delimiter)
|
314
|
+
end
|
315
|
+
return filter_process_param_array(array, attr_metadata, opts) unless array.nil?
|
316
|
+
operator, value = parse_filter_operator(raw_value)
|
317
|
+
[[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
|
318
|
+
end
|
319
|
+
|
320
|
+
def filter_process_param_array(array, attr_metadata, opts)
|
321
|
+
operator_value_pairs = []
|
322
|
+
equals_values = []
|
323
|
+
array.map(&:strip).reject(&:blank?).each do |value|
|
324
|
+
operator, value = parse_filter_operator(value)
|
325
|
+
value = ModelApi::Utils.transform_value(value.to_s, attr_metadata[:parse], opts)
|
326
|
+
if operator == '='
|
327
|
+
equals_values << value
|
328
|
+
else
|
329
|
+
operator_value_pairs << [operator, value]
|
330
|
+
end
|
331
|
+
end
|
332
|
+
operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
|
333
|
+
operator_value_pairs
|
334
|
+
end
|
335
|
+
|
336
|
+
def parse_filter_operator(value)
|
337
|
+
value = value.to_s.strip
|
338
|
+
if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
|
339
|
+
return (operator == '<>' ? '!=' : operator), value[2..-1].strip
|
340
|
+
elsif (operator = value.scan(/\A(>|<|=)[[:space:]]*\w/).flatten.first).present?
|
341
|
+
return operator, value[1..-1].strip
|
342
|
+
end
|
343
|
+
['=', value]
|
344
|
+
end
|
345
|
+
|
346
|
+
def format_value_for_query(column, value, klass)
|
347
|
+
return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
|
348
|
+
column_metadata = klass.columns_hash[column.to_s]
|
349
|
+
case column_metadata.try(:type)
|
350
|
+
when :date, :datetime, :time, :timestamp
|
351
|
+
user = current_user
|
352
|
+
if user.respond_to?(:time_zone) && (user_time_zone = user.time_zone).present?
|
353
|
+
time_zone = ActiveSupport::TimeZone.new(user_time_zone)
|
354
|
+
end
|
355
|
+
time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
|
356
|
+
return time_zone.parse(value.to_s).try(:to_s, :db)
|
357
|
+
when :float, :decimal
|
358
|
+
return value.to_d.to_s
|
359
|
+
when :integer, :primary_key
|
360
|
+
return value.to_d.to_s.sub(/\.0\Z/, '')
|
361
|
+
when :boolean
|
362
|
+
return value ? 'true' : 'false'
|
363
|
+
end
|
364
|
+
value.to_s
|
365
|
+
end
|
366
|
+
|
367
|
+
def params_array(raw_value)
|
368
|
+
index = 0
|
369
|
+
array = []
|
370
|
+
while raw_value.include?(index.to_s)
|
371
|
+
array << raw_value[index.to_s]
|
372
|
+
index += 1
|
373
|
+
end
|
374
|
+
array
|
375
|
+
end
|
376
|
+
|
377
|
+
def resolve_key_to_column(klass, attr_metadata)
|
378
|
+
return nil unless klass.respond_to?(:columns_hash)
|
379
|
+
columns_hash = klass.columns_hash
|
380
|
+
key = attr_metadata[:key]
|
381
|
+
return key if columns_hash.include?(key.to_s)
|
382
|
+
render_method = attr_metadata[:render_method]
|
383
|
+
render_method = render_method.to_s if render_method.is_a?(Symbol)
|
384
|
+
return nil unless render_method.is_a?(String)
|
385
|
+
columns_hash.include?(render_method) ? render_method : nil
|
386
|
+
end
|
387
|
+
|
388
|
+
def class_or_sti_subclass(klass, req_body, operation, opts = {})
|
389
|
+
metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
|
390
|
+
if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
|
391
|
+
req_body.is_a?(Hash)
|
392
|
+
external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
|
393
|
+
type = req_body[external_attr.to_s]
|
394
|
+
begin
|
395
|
+
type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
|
396
|
+
rescue Exception => e
|
397
|
+
Rails.logger.warn 'Error encountered parsing API input for attribute ' \
|
398
|
+
"\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
|
399
|
+
'using raw value instead.'
|
400
|
+
end
|
401
|
+
if type.present? && (type = type.camelize) != klass.name
|
402
|
+
Rails.application.eager_load!
|
403
|
+
klass.subclasses.each do |subclass|
|
404
|
+
return subclass if subclass.name == type
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
klass
|
409
|
+
end
|
410
|
+
|
411
|
+
def object_from_req_body(root_elem, req_body, format)
|
412
|
+
if format == :json
|
413
|
+
request_obj = req_body
|
414
|
+
else
|
415
|
+
request_obj = req_body[root_elem]
|
416
|
+
if request_obj.blank?
|
417
|
+
request_obj = req_body['obj']
|
418
|
+
if request_obj.blank? && req_body.size == 1
|
419
|
+
request_obj = req_body.values.first
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
fail 'Invalid request format' unless request_obj.present?
|
424
|
+
request_obj
|
425
|
+
end
|
426
|
+
|
427
|
+
def add_pagination_links(collection_links, coll_route, page, last_page)
|
428
|
+
if page < last_page
|
429
|
+
collection_links[:next] = [coll_route, { page: (page + 1) }]
|
430
|
+
end
|
431
|
+
collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
|
432
|
+
collection_links[:first] = [coll_route, { page: 1 }]
|
433
|
+
collection_links[:last] = [coll_route, { page: last_page }]
|
434
|
+
end
|
435
|
+
|
436
|
+
def verify_update_request_body(request_body, format, opts = {})
|
437
|
+
if request_body.is_a?(Array)
|
438
|
+
fail 'Expected object, but collection provided'
|
439
|
+
elsif !request_body.is_a?(Hash)
|
440
|
+
fail 'Expected object'
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'model-api/api_context'
|
2
|
+
|
1
3
|
module ModelApi
|
2
4
|
module BaseController
|
3
5
|
module ClassMethods
|
@@ -39,20 +41,25 @@ module ModelApi
|
|
39
41
|
self.class.model_class
|
40
42
|
end
|
41
43
|
|
44
|
+
def api_context
|
45
|
+
@api_context ||= ModelApi::ApiContext.new(self)
|
46
|
+
end
|
47
|
+
|
42
48
|
def render_collection(collection, opts = {})
|
43
49
|
return unless ensure_admin_if_admin_only(opts)
|
44
|
-
opts = prepare_options(opts)
|
50
|
+
opts = api_context.prepare_options(opts)
|
45
51
|
opts[:operation] ||= :index
|
46
|
-
return unless validate_read_operation(collection, opts[:operation], opts)
|
52
|
+
return unless api_context.validate_read_operation(collection, opts[:operation], opts)
|
47
53
|
|
48
54
|
coll_route = opts[:collection_route] || self
|
49
55
|
collection_links = { self: coll_route }
|
50
56
|
collection = ModelApi::Utils.process_collection_includes(collection,
|
51
57
|
opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
|
52
|
-
collection, _result_filters = filter_collection(collection, find_filter_params,
|
53
|
-
|
54
|
-
collection,
|
55
|
-
|
58
|
+
collection, _result_filters = api_context.filter_collection(collection, find_filter_params,
|
59
|
+
opts)
|
60
|
+
collection, _result_sorts = api_context.sort_collection(collection, find_sort_params, opts)
|
61
|
+
collection, collection_links, opts = paginate_collection(collection, collection_links, opts,
|
62
|
+
coll_route)
|
56
63
|
|
57
64
|
opts[:collection_links] = collection_links.merge(opts[:collection_links] || {})
|
58
65
|
.reverse_merge(common_response_links(opts))
|
@@ -62,13 +69,13 @@ module ModelApi
|
|
62
69
|
|
63
70
|
def render_object(obj, opts = {})
|
64
71
|
return unless ensure_admin_if_admin_only(opts)
|
65
|
-
opts = prepare_options(opts)
|
72
|
+
opts = api_context.prepare_options(opts)
|
66
73
|
klass = ModelApi::Utils.find_class(obj, opts)
|
67
74
|
object_route = opts[:object_route] || self
|
68
75
|
|
69
76
|
opts[:object_links] = { self: object_route }
|
70
77
|
if obj.is_a?(ActiveRecord::Base)
|
71
|
-
return unless validate_read_operation(obj, opts[:operation], opts)
|
78
|
+
return unless api_context.validate_read_operation(obj, opts[:operation], opts)
|
72
79
|
unless obj.present?
|
73
80
|
return not_found(opts.merge(class: klass, field: :id))
|
74
81
|
end
|
@@ -96,12 +103,16 @@ module ModelApi
|
|
96
103
|
end
|
97
104
|
|
98
105
|
def prepare_object_for_create(klass, opts = {})
|
99
|
-
opts = prepare_options(opts)
|
100
|
-
|
106
|
+
opts = api_context.prepare_options(opts)
|
107
|
+
req_body, format = parse_request_body
|
108
|
+
opts = add_hateoas_links_for_update(opts)
|
109
|
+
api_context.get_updated_object(klass, get_operation(:create, opts), req_body,
|
110
|
+
opts.merge(format: format))
|
111
|
+
|
101
112
|
end
|
102
113
|
|
103
114
|
def create_and_render_object(obj, opts = {})
|
104
|
-
opts = prepare_options(opts)
|
115
|
+
opts = api_context.prepare_options(opts)
|
105
116
|
object_link_options = opts[:object_link_options]
|
106
117
|
object_link_options[:action] = :show
|
107
118
|
save_and_render_object(obj, get_operation(:create, opts), opts.merge(location_header: true))
|
@@ -118,17 +129,20 @@ module ModelApi
|
|
118
129
|
end
|
119
130
|
|
120
131
|
def prepare_object_for_update(obj, opts = {})
|
121
|
-
opts = prepare_options(opts)
|
122
|
-
|
132
|
+
opts = api_context.prepare_options(opts)
|
133
|
+
req_body, format = parse_request_body
|
134
|
+
opts = add_hateoas_links_for_update(opts)
|
135
|
+
api_context.get_updated_object(obj, get_operation(:update, opts), req_body,
|
136
|
+
opts.merge(format: format))
|
123
137
|
end
|
124
138
|
|
125
139
|
def update_and_render_object(obj, opts = {})
|
126
|
-
opts = prepare_options(opts)
|
140
|
+
opts = api_context.prepare_options(opts)
|
127
141
|
save_and_render_object(obj, get_operation(:update, opts), opts)
|
128
142
|
end
|
129
143
|
|
130
144
|
def save_and_render_object(obj, operation, opts = {})
|
131
|
-
opts = prepare_options(opts)
|
145
|
+
opts = api_context.prepare_options(opts)
|
132
146
|
status, msgs = Utils.process_updated_model_save(obj, operation, opts)
|
133
147
|
add_hateoas_links_for_updated_object(operation, opts)
|
134
148
|
successful = ModelApi::Utils.response_successful?(status)
|
@@ -138,7 +152,7 @@ module ModelApi
|
|
138
152
|
|
139
153
|
def do_destroy(obj, opts = {})
|
140
154
|
return unless ensure_admin_if_admin_only(opts)
|
141
|
-
opts = prepare_options(opts)
|
155
|
+
opts = api_context.prepare_options(opts)
|
142
156
|
obj = obj.first if obj.is_a?(ActiveRecord::Relation)
|
143
157
|
|
144
158
|
add_hateoas_links_for_update(opts)
|
@@ -164,6 +178,7 @@ module ModelApi
|
|
164
178
|
def initialize_options(opts)
|
165
179
|
return opts if opts[:options_initialized]
|
166
180
|
opts = opts.symbolize_keys
|
181
|
+
opts[:api_context] ||= @api_context
|
167
182
|
opts[:model_class] ||= model_class
|
168
183
|
opts[:user] ||= filter_by_user
|
169
184
|
opts[:user_id] ||= opts[:user].try(:id)
|
@@ -193,81 +208,26 @@ module ModelApi
|
|
193
208
|
id_info
|
194
209
|
end
|
195
210
|
|
196
|
-
def api_query(opts = {})
|
197
|
-
opts = prepare_options(opts)
|
198
|
-
klass = opts[:model_class] || model_class
|
199
|
-
model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(klass)
|
200
|
-
unless klass < ActiveRecord::Base
|
201
|
-
fail 'Expected model class to be an ActiveRecord::Base subclass'
|
202
|
-
end
|
203
|
-
query = ModelApi::Utils.invoke_callback(model_metadata[:base_query], opts) || klass.all
|
204
|
-
if (deleted_col = klass.columns_hash['deleted']).present?
|
205
|
-
case deleted_col.type
|
206
|
-
when :boolean
|
207
|
-
query = query.where(deleted: false)
|
208
|
-
when :integer, :decimal
|
209
|
-
query = query.where(deleted: 0)
|
210
|
-
end
|
211
|
-
end
|
212
|
-
Utils.apply_context(query, opts)
|
213
|
-
end
|
214
|
-
|
215
211
|
def common_object_query(opts = {})
|
216
|
-
|
217
|
-
coll_query = Utils.apply_context(api_query(opts), opts)
|
212
|
+
opts = api_context.prepare_options(opts)
|
218
213
|
id_info = opts[:id_info] || id_info(opts)
|
219
|
-
|
220
|
-
|
221
|
-
unless opts.include?(:user_filter) && !opts[:user_filter]
|
222
|
-
query = user_query(query, opts.merge(model_class: klass))
|
223
|
-
end
|
224
|
-
elsif id_info[:id_attribute] != :id && !id_info[:id_attribute].to_s.ends_with?('.id') &&
|
225
|
-
klass.column_names.include?('id') && !query.exists?
|
226
|
-
# Admins can optionally use record ID's if the ID field happens to be something else
|
227
|
-
query = coll_query.where(id: id_info[:id_value])
|
228
|
-
end
|
229
|
-
unless (not_found_error = opts[:not_found_error]).blank? || query.exists?
|
230
|
-
not_found_error = not_found_error.call(params[:id]) if not_found_error.respond_to?(:call)
|
231
|
-
if not_found_error == true
|
232
|
-
not_found_error = "#{klass.model_name.human} '#{id_info[:id_value]}' not found."
|
233
|
-
end
|
234
|
-
fail ModelApi::NotFoundException.new(id_info[:id_param], not_found_error.to_s)
|
235
|
-
end
|
236
|
-
query
|
214
|
+
api_context.common_object_query(id_info[:id_attribute], id_info[:id_value],
|
215
|
+
opts.merge(id_param: id_info[:id_param]))
|
237
216
|
end
|
238
217
|
|
239
218
|
def collection_query(opts = {})
|
240
|
-
opts = base_api_options.merge(opts)
|
219
|
+
opts = api_context.prepare_options(base_api_options.merge(opts))
|
241
220
|
klass = opts[:model_class] || model_class
|
242
|
-
query = api_query(opts)
|
221
|
+
query = api_context.api_query(klass, opts)
|
243
222
|
unless (opts.include?(:user_filter) && !opts[:user_filter]) ||
|
244
|
-
(admin? || filtered_by_foreign_key?(query))
|
245
|
-
query = user_query(query, opts.merge(model_class: klass))
|
223
|
+
(admin? || filtered_by_foreign_key?(query)) || !opts[:user]
|
224
|
+
query = api_context.user_query(query, opts[:user], opts.merge(model_class: klass))
|
246
225
|
end
|
247
226
|
query
|
248
227
|
end
|
249
228
|
|
250
229
|
def object_query(opts = {})
|
251
|
-
common_object_query(base_api_options.merge(opts))
|
252
|
-
end
|
253
|
-
|
254
|
-
def user_query(query, opts = {})
|
255
|
-
user = opts[:user] || filter_by_user
|
256
|
-
klass = opts[:model_class] || query.klass
|
257
|
-
user_id_col = opts[:user_id_column] || :user_id
|
258
|
-
user_assoc = opts[:user_association] || :user
|
259
|
-
user_id = user.try(opts[:user_id_attribute] || :id)
|
260
|
-
if klass.columns_hash.include?(user_id_col.to_s)
|
261
|
-
query = query.where(user_id_col => user_id)
|
262
|
-
elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
|
263
|
-
[:belongs_to, :has_one].include?(assoc.macro)
|
264
|
-
query = query.joins(user_assoc).where(
|
265
|
-
"#{assoc.klass.table_name}.#{assoc.klass.primary_key}" => user_id)
|
266
|
-
elsif opts[:user_filter]
|
267
|
-
fail "Unable to filter results by user; no '#{user_id_col}' column or " \
|
268
|
-
"'#{user_assoc}' association found!"
|
269
|
-
end
|
270
|
-
query
|
230
|
+
common_object_query(api_context.prepare_options(base_api_options.merge(opts)))
|
271
231
|
end
|
272
232
|
|
273
233
|
def base_api_options
|
@@ -344,6 +304,7 @@ module ModelApi
|
|
344
304
|
end
|
345
305
|
|
346
306
|
def resource_parent_id(parent_model_class, opts = {})
|
307
|
+
opts = api_context.prepare_options(opts)
|
347
308
|
id_info = id_info(opts.reverse_merge(id_param: "#{parent_model_class.name.underscore}_id"))
|
348
309
|
model_name = parent_model_class.model_name.human
|
349
310
|
if id_info[:id_value].blank?
|
@@ -421,21 +382,6 @@ module ModelApi
|
|
421
382
|
simple_error(:not_implemented, opts.delete(:error) || 'Not implemented', opts)
|
422
383
|
end
|
423
384
|
|
424
|
-
def validate_read_operation(obj, operation, opts = {})
|
425
|
-
opts = prepare_options(opts)
|
426
|
-
status, errors = ModelApi::Utils.validate_operation(obj, operation,
|
427
|
-
opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
|
428
|
-
return true if status.nil? && errors.nil?
|
429
|
-
if errors.nil? && (status.is_a?(Array) || status.present?)
|
430
|
-
return true if (errors = status).blank?
|
431
|
-
status = :bad_request
|
432
|
-
end
|
433
|
-
return true unless errors.present?
|
434
|
-
errors = [errors] unless errors.is_a?(Array)
|
435
|
-
simple_error(status, errors, opts)
|
436
|
-
false
|
437
|
-
end
|
438
|
-
|
439
385
|
def filter_by_user
|
440
386
|
current_user
|
441
387
|
end
|
@@ -476,32 +422,11 @@ module ModelApi
|
|
476
422
|
end
|
477
423
|
end
|
478
424
|
|
479
|
-
def
|
480
|
-
|
481
|
-
|
482
|
-
req_body, format = ModelApi::Utils.parse_request_body(request)
|
483
|
-
if obj_or_class.is_a?(Class)
|
484
|
-
klass = Utils.class_or_sti_subclass(obj_or_class, req_body, operation, opts)
|
485
|
-
obj = nil
|
486
|
-
elsif obj_or_class.is_a?(ActiveRecord::Base)
|
487
|
-
obj = obj_or_class
|
488
|
-
klass = obj.class
|
489
|
-
elsif obj_or_class.is_a?(ActiveRecord::Relation)
|
490
|
-
klass = obj_or_class.klass
|
491
|
-
obj = obj_or_class.first
|
425
|
+
def parse_request_body
|
426
|
+
unless instance_variable_defined?(:@request_body)
|
427
|
+
@req_body, @format = ModelApi::Utils.parse_request_body(request)
|
492
428
|
end
|
493
|
-
|
494
|
-
opts[:api_model_metadata] = model_metadata = ModelApi::Utils.model_metadata(klass)
|
495
|
-
opts[:ignored_fields] = []
|
496
|
-
return [nil, opts.merge(bad_payload: true)] if req_body.nil?
|
497
|
-
obj = klass.new if obj.nil?
|
498
|
-
add_hateoas_links_for_update(opts)
|
499
|
-
verify_update_request_body(req_body, format, opts)
|
500
|
-
root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
|
501
|
-
request_obj = opts[:request_obj] = Utils.object_from_req_body(root_elem, req_body, format)
|
502
|
-
ModelApi::Utils.apply_updates(obj, request_obj, operation, opts)
|
503
|
-
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
|
504
|
-
[obj, opts]
|
429
|
+
[@req_body, @format]
|
505
430
|
end
|
506
431
|
|
507
432
|
private
|
@@ -572,248 +497,6 @@ module ModelApi
|
|
572
497
|
sort_params
|
573
498
|
end
|
574
499
|
|
575
|
-
def filter_collection(collection, filter_params, opts = {})
|
576
|
-
return [collection, {}] if filter_params.blank? # Don't filter if no filter params
|
577
|
-
klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
|
578
|
-
assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
|
579
|
-
result_filters = {}
|
580
|
-
metadata.values.each do |attr_metadata|
|
581
|
-
collection = apply_filter_param(attr_metadata, collection,
|
582
|
-
opts.merge(attr_values: attr_values, result_filters: result_filters, class: klass))
|
583
|
-
end
|
584
|
-
assoc_values.each do |assoc, assoc_filter_params|
|
585
|
-
ar_assoc = klass.reflect_on_association(assoc)
|
586
|
-
next unless ar_assoc.present?
|
587
|
-
collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
|
588
|
-
collection, assoc_result_filters = filter_collection(collection, assoc_filter_params,
|
589
|
-
opts.merge(class: ar_assoc.klass, filter_table: ar_assoc.table_name))
|
590
|
-
result_filters[assoc] = assoc_result_filters if assoc_result_filters.present?
|
591
|
-
end
|
592
|
-
[collection, result_filters]
|
593
|
-
end
|
594
|
-
|
595
|
-
def process_filter_params(filter_params, klass, opts = {})
|
596
|
-
assoc_values = {}
|
597
|
-
filter_metadata = {}
|
598
|
-
attr_values = {}
|
599
|
-
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :filter, opts)
|
600
|
-
filter_params.each do |attr, value|
|
601
|
-
attr = attr.to_s
|
602
|
-
if attr.length > 1 && ['>', '<', '!', '='].include?(attr[-1])
|
603
|
-
value = "#{attr[-1]}=#{value}" # Effectively allows >= / <= / != / == in query string
|
604
|
-
attr = attr[0..-2].strip
|
605
|
-
end
|
606
|
-
if attr.include?('.')
|
607
|
-
process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
608
|
-
else
|
609
|
-
process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
610
|
-
end
|
611
|
-
end
|
612
|
-
[assoc_values, filter_metadata, attr_values]
|
613
|
-
end
|
614
|
-
|
615
|
-
# rubocop:disable Metrics/ParameterLists
|
616
|
-
def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
|
617
|
-
attr_elems = attr.split('.')
|
618
|
-
assoc_name = attr_elems[0].strip.to_sym
|
619
|
-
assoc_metadata = metadata[assoc_name] ||
|
620
|
-
metadata[ModelApi::Utils.ext_query_attr(assoc_name, opts)] || {}
|
621
|
-
key = assoc_metadata[:key]
|
622
|
-
return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:filter], opts)
|
623
|
-
assoc_filter_params = (assoc_values[key] ||= {})
|
624
|
-
assoc_filter_params[attr_elems[1..-1].join('.')] = value
|
625
|
-
end
|
626
|
-
|
627
|
-
def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
|
628
|
-
attr = attr.strip.to_sym
|
629
|
-
attr_metadata = metadata[attr] ||
|
630
|
-
metadata[ModelApi::Utils.ext_query_attr(attr, opts)] || {}
|
631
|
-
key = attr_metadata[:key]
|
632
|
-
return unless key.present? && ModelApi::Utils.eval_bool(attr_metadata[:filter], opts)
|
633
|
-
filter_metadata[key] = attr_metadata
|
634
|
-
attr_values[key] = value
|
635
|
-
end
|
636
|
-
|
637
|
-
# rubocop:enable Metrics/ParameterLists
|
638
|
-
|
639
|
-
def apply_filter_param(attr_metadata, collection, opts = {})
|
640
|
-
raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
|
641
|
-
filter_table = opts[:filter_table]
|
642
|
-
klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
|
643
|
-
if raw_value.is_a?(Hash) && raw_value.include?('0')
|
644
|
-
operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
|
645
|
-
opts)
|
646
|
-
else
|
647
|
-
operator_value_pairs = filter_process_param(raw_value, attr_metadata, opts)
|
648
|
-
end
|
649
|
-
if (column = resolve_key_to_column(klass, attr_metadata)).present?
|
650
|
-
operator_value_pairs.each do |operator, value|
|
651
|
-
if operator == '=' && filter_table.blank?
|
652
|
-
collection = collection.where(column => value)
|
653
|
-
else
|
654
|
-
table_name = (filter_table || klass.table_name).to_s.delete('`')
|
655
|
-
column = column.to_s.delete('`')
|
656
|
-
if value.is_a?(Array)
|
657
|
-
operator = 'IN'
|
658
|
-
value = value.map { |_v| format_value_for_query(column, value, klass) }
|
659
|
-
value = "(#{value.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')})"
|
660
|
-
else
|
661
|
-
value = "'#{value.gsub("'", "''")}'"
|
662
|
-
end
|
663
|
-
collection = collection.where("`#{table_name}`.`#{column}` #{operator} #{value}")
|
664
|
-
end
|
665
|
-
end
|
666
|
-
elsif (key = attr_metadata[:key]).present?
|
667
|
-
opts[:result_filters][key] = operator_value_pairs if opts.include?(:result_filters)
|
668
|
-
end
|
669
|
-
collection
|
670
|
-
end
|
671
|
-
|
672
|
-
def sort_collection(collection, sort_params, opts = {})
|
673
|
-
return [collection, {}] if sort_params.blank? # Don't filter if no filter params
|
674
|
-
klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
|
675
|
-
assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
|
676
|
-
opts.merge(result_sorts: result_sorts))
|
677
|
-
sort_table = opts[:sort_table]
|
678
|
-
sort_table = sort_table.to_s.delete('`') if sort_table.present?
|
679
|
-
attr_sorts.each do |key, sort_order|
|
680
|
-
if sort_table.present?
|
681
|
-
collection = collection.order("`#{sort_table}`.`#{key.to_s.delete('`')}` " \
|
682
|
-
"#{sort_order.to_s.upcase}")
|
683
|
-
else
|
684
|
-
collection = collection.order(key => sort_order)
|
685
|
-
end
|
686
|
-
end
|
687
|
-
assoc_sorts.each do |assoc, assoc_sort_params|
|
688
|
-
ar_assoc = klass.reflect_on_association(assoc)
|
689
|
-
next unless ar_assoc.present?
|
690
|
-
collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
|
691
|
-
collection, assoc_result_sorts = sort_collection(collection, assoc_sort_params,
|
692
|
-
opts.merge(class: ar_assoc.klass, sort_table: ar_assoc.table_name))
|
693
|
-
result_sorts[assoc] = assoc_result_sorts if assoc_result_sorts.present?
|
694
|
-
end
|
695
|
-
[collection, result_sorts]
|
696
|
-
end
|
697
|
-
|
698
|
-
def process_sort_params(sort_params, klass, opts)
|
699
|
-
metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
|
700
|
-
assoc_sorts = {}
|
701
|
-
attr_sorts = {}
|
702
|
-
result_sorts = {}
|
703
|
-
sort_params.each do |attr, sort_order|
|
704
|
-
if attr.include?('.')
|
705
|
-
process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
706
|
-
else
|
707
|
-
attr = attr.strip.to_sym
|
708
|
-
attr_metadata = metadata[attr] || {}
|
709
|
-
next unless ModelApi::Utils.eval_bool(attr_metadata[:sort], opts)
|
710
|
-
sort_order = sort_order.to_sym
|
711
|
-
sort_order = :default unless [:asc, :desc].include?(sort_order)
|
712
|
-
if sort_order == :default
|
713
|
-
sort_order = (attr_metadata[:default_sort_order] || :asc).to_sym
|
714
|
-
sort_order = :asc unless [:asc, :desc].include?(sort_order)
|
715
|
-
end
|
716
|
-
if (column = resolve_key_to_column(klass, attr_metadata)).present?
|
717
|
-
attr_sorts[column] = sort_order
|
718
|
-
elsif (key = attr_metadata[:key]).present?
|
719
|
-
result_sorts[key] = sort_order
|
720
|
-
end
|
721
|
-
end
|
722
|
-
end
|
723
|
-
[assoc_sorts, attr_sorts, result_sorts]
|
724
|
-
end
|
725
|
-
|
726
|
-
# Intentionally disabling parameter list length check for private / internal method
|
727
|
-
# rubocop:disable Metrics/ParameterLists
|
728
|
-
def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
|
729
|
-
attr_elems = attr.split('.')
|
730
|
-
assoc_name = attr_elems[0].strip.to_sym
|
731
|
-
assoc_metadata = metadata[assoc_name] || {}
|
732
|
-
key = assoc_metadata[:key]
|
733
|
-
return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:sort], opts)
|
734
|
-
assoc_sort_params = (assoc_sorts[key] ||= {})
|
735
|
-
assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
|
736
|
-
end
|
737
|
-
|
738
|
-
# rubocop:enable Metrics/ParameterLists
|
739
|
-
|
740
|
-
def filter_process_param(raw_value, attr_metadata, opts)
|
741
|
-
raw_value = raw_value.to_s.strip
|
742
|
-
array = nil
|
743
|
-
if raw_value.starts_with?('[') && raw_value.ends_with?(']')
|
744
|
-
array = JSON.parse(raw_value) rescue nil
|
745
|
-
array = array.is_a?(Array) ? array.map(&:to_s) : nil
|
746
|
-
end
|
747
|
-
if array.nil?
|
748
|
-
if attr_metadata.include?(:filter_delimiter)
|
749
|
-
delimiter = attr_metadata[:filter_delimiter]
|
750
|
-
else
|
751
|
-
delimiter = ','
|
752
|
-
end
|
753
|
-
array = raw_value.split(delimiter) if raw_value.include?(delimiter)
|
754
|
-
end
|
755
|
-
return filter_process_param_array(array, attr_metadata, opts) unless array.nil?
|
756
|
-
operator, value = parse_filter_operator(raw_value)
|
757
|
-
[[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
|
758
|
-
end
|
759
|
-
|
760
|
-
def filter_process_param_array(array, attr_metadata, opts)
|
761
|
-
operator_value_pairs = []
|
762
|
-
equals_values = []
|
763
|
-
array.map(&:strip).reject(&:blank?).each do |value|
|
764
|
-
operator, value = parse_filter_operator(value)
|
765
|
-
value = ModelApi::Utils.transform_value(value.to_s, attr_metadata[:parse], opts)
|
766
|
-
if operator == '='
|
767
|
-
equals_values << value
|
768
|
-
else
|
769
|
-
operator_value_pairs << [operator, value]
|
770
|
-
end
|
771
|
-
end
|
772
|
-
operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
|
773
|
-
operator_value_pairs
|
774
|
-
end
|
775
|
-
|
776
|
-
def parse_filter_operator(value)
|
777
|
-
value = value.to_s.strip
|
778
|
-
if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
|
779
|
-
return (operator == '<>' ? '!=' : operator), value[2..-1].strip
|
780
|
-
elsif (operator = value.scan(/\A(>|<|=)[[:space:]]*\w/).flatten.first).present?
|
781
|
-
return operator, value[1..-1].strip
|
782
|
-
end
|
783
|
-
['=', value]
|
784
|
-
end
|
785
|
-
|
786
|
-
def format_value_for_query(column, value, klass)
|
787
|
-
return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
|
788
|
-
column_metadata = klass.columns_hash[column.to_s]
|
789
|
-
case column_metadata.try(:type)
|
790
|
-
when :date, :datetime, :time, :timestamp
|
791
|
-
user = current_user
|
792
|
-
if user.respond_to?(:time_zone) && (user_time_zone = user.time_zone).present?
|
793
|
-
time_zone = ActiveSupport::TimeZone.new(user_time_zone)
|
794
|
-
end
|
795
|
-
time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
|
796
|
-
return time_zone.parse(value.to_s).try(:to_s, :db)
|
797
|
-
when :float, :decimal
|
798
|
-
return value.to_d.to_s
|
799
|
-
when :integer, :primary_key
|
800
|
-
return value.to_d.to_s.sub(/\.0\Z/, '')
|
801
|
-
when :boolean
|
802
|
-
return value ? 'true' : 'false'
|
803
|
-
end
|
804
|
-
value.to_s
|
805
|
-
end
|
806
|
-
|
807
|
-
def params_array(raw_value)
|
808
|
-
index = 0
|
809
|
-
array = []
|
810
|
-
while raw_value.include?(index.to_s)
|
811
|
-
array << raw_value[index.to_s]
|
812
|
-
index += 1
|
813
|
-
end
|
814
|
-
array
|
815
|
-
end
|
816
|
-
|
817
500
|
def paginate_collection(collection, collection_links, opts, coll_route)
|
818
501
|
collection_size = collection.count
|
819
502
|
page_size = (params[:page_size] || DEFAULT_PAGE_SIZE).to_i
|
@@ -837,24 +520,13 @@ module ModelApi
|
|
837
520
|
|
838
521
|
if collection_size > page_size
|
839
522
|
opts[:collection_link_options][:page] = page
|
840
|
-
|
523
|
+
add_pagination_links(collection_links, coll_route, page, page_count)
|
841
524
|
collection = collection.limit(page_size).offset(offset)
|
842
525
|
end
|
843
526
|
|
844
527
|
[collection, collection_links, opts]
|
845
528
|
end
|
846
529
|
|
847
|
-
def resolve_key_to_column(klass, attr_metadata)
|
848
|
-
return nil unless klass.respond_to?(:columns_hash)
|
849
|
-
columns_hash = klass.columns_hash
|
850
|
-
key = attr_metadata[:key]
|
851
|
-
return key if columns_hash.include?(key.to_s)
|
852
|
-
render_method = attr_metadata[:render_method]
|
853
|
-
render_method = render_method.to_s if render_method.is_a?(Symbol)
|
854
|
-
return nil unless render_method.is_a?(String)
|
855
|
-
columns_hash.include?(render_method) ? render_method : nil
|
856
|
-
end
|
857
|
-
|
858
530
|
def add_collection_object_route(opts)
|
859
531
|
object_route = opts[:object_route]
|
860
532
|
unless object_route.present?
|
@@ -877,6 +549,7 @@ module ModelApi
|
|
877
549
|
object_route = opts[:object_route] || self
|
878
550
|
links = { self: object_route }.reverse_merge(common_response_links(opts))
|
879
551
|
opts[:links] = links.merge(opts[:links] || {})
|
552
|
+
opts
|
880
553
|
end
|
881
554
|
|
882
555
|
def add_hateoas_links_for_updated_object(_operation, opts)
|
@@ -885,18 +558,6 @@ module ModelApi
|
|
885
558
|
opts[:object_links] = object_links.merge(opts[:object_links] || {})
|
886
559
|
end
|
887
560
|
|
888
|
-
def verify_update_request_body(request_body, format, opts = {})
|
889
|
-
if request.format.symbol.nil? && format.present?
|
890
|
-
opts[:format] ||= format
|
891
|
-
end
|
892
|
-
|
893
|
-
if request_body.is_a?(Array)
|
894
|
-
fail 'Expected object, but collection provided'
|
895
|
-
elsif !request_body.is_a?(Hash)
|
896
|
-
fail 'Expected object'
|
897
|
-
end
|
898
|
-
end
|
899
|
-
|
900
561
|
def filtered_by_foreign_key?(query)
|
901
562
|
fk_cache = self.class.instance_variable_get(:@foreign_key_cache)
|
902
563
|
self.class.instance_variable_set(:@foreign_key_cache, fk_cache = {}) if fk_cache.nil?
|
@@ -916,31 +577,6 @@ module ModelApi
|
|
916
577
|
|
917
578
|
class Utils
|
918
579
|
class << self
|
919
|
-
def add_pagination_links(collection_links, coll_route, page, last_page)
|
920
|
-
if page < last_page
|
921
|
-
collection_links[:next] = [coll_route, { page: (page + 1) }]
|
922
|
-
end
|
923
|
-
collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
|
924
|
-
collection_links[:first] = [coll_route, { page: 1 }]
|
925
|
-
collection_links[:last] = [coll_route, { page: last_page }]
|
926
|
-
end
|
927
|
-
|
928
|
-
def object_from_req_body(root_elem, req_body, format)
|
929
|
-
if format == :json
|
930
|
-
request_obj = req_body
|
931
|
-
else
|
932
|
-
request_obj = req_body[root_elem]
|
933
|
-
if request_obj.blank?
|
934
|
-
request_obj = req_body['obj']
|
935
|
-
if request_obj.blank? && req_body.size == 1
|
936
|
-
request_obj = req_body.values.first
|
937
|
-
end
|
938
|
-
end
|
939
|
-
end
|
940
|
-
fail 'Invalid request format' unless request_obj.present?
|
941
|
-
request_obj
|
942
|
-
end
|
943
|
-
|
944
580
|
def process_updated_model_save(obj, operation, opts = {})
|
945
581
|
opts = opts.dup
|
946
582
|
opts[:operation] = operation
|
@@ -1012,40 +648,6 @@ module ModelApi
|
|
1012
648
|
Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
|
1013
649
|
false
|
1014
650
|
end
|
1015
|
-
|
1016
|
-
def apply_context(query, opts = {})
|
1017
|
-
context = opts[:context]
|
1018
|
-
return query if context.nil?
|
1019
|
-
if context.respond_to?(:call)
|
1020
|
-
query = context.send(*([:call, query, opts][0..context.parameters.size]))
|
1021
|
-
elsif context.is_a?(Hash)
|
1022
|
-
context.each { |attr, value| query = query.where(attr => value) }
|
1023
|
-
end
|
1024
|
-
query
|
1025
|
-
end
|
1026
|
-
|
1027
|
-
def class_or_sti_subclass(klass, req_body, operation, opts = {})
|
1028
|
-
metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
|
1029
|
-
if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
|
1030
|
-
req_body.is_a?(Hash)
|
1031
|
-
external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
|
1032
|
-
type = req_body[external_attr.to_s]
|
1033
|
-
begin
|
1034
|
-
type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
|
1035
|
-
rescue Exception => e
|
1036
|
-
Rails.logger.warn 'Error encountered parsing API input for attribute ' \
|
1037
|
-
"\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
|
1038
|
-
'using raw value instead.'
|
1039
|
-
end
|
1040
|
-
if type.present? && (type = type.camelize) != klass.name
|
1041
|
-
Rails.application.eager_load!
|
1042
|
-
klass.subclasses.each do |subclass|
|
1043
|
-
return subclass if subclass.name == type
|
1044
|
-
end
|
1045
|
-
end
|
1046
|
-
end
|
1047
|
-
klass
|
1048
|
-
end
|
1049
651
|
end
|
1050
652
|
end
|
1051
653
|
end
|
data/model-api.gemspec
CHANGED
@@ -3,7 +3,7 @@ $:.unshift lib unless $:.include?(lib)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = 'model-api'
|
6
|
-
s.version = '0.8.
|
6
|
+
s.version = '0.8.8'
|
7
7
|
s.summary = 'Create easy REST API\'s using metadata inside your ActiveRecord models'
|
8
8
|
s.description = 'Ruby gem allowing Ruby on Rails developers to create REST API’s using ' \
|
9
9
|
'metadata defined inside their ActiveRecord models.'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: model-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Mead
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -67,6 +67,7 @@ extra_rdoc_files: []
|
|
67
67
|
files:
|
68
68
|
- README.md
|
69
69
|
- lib/model-api.rb
|
70
|
+
- lib/model-api/api_context.rb
|
70
71
|
- lib/model-api/base_controller.rb
|
71
72
|
- lib/model-api/bypass_parse_middleware.rb
|
72
73
|
- lib/model-api/hash_metadata.rb
|