better_model 1.0.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/MIT-LICENSE +20 -0
- data/README.md +312 -0
- data/Rakefile +12 -0
- data/lib/better_model/archivable.rb +272 -0
- data/lib/better_model/permissible.rb +151 -0
- data/lib/better_model/predicable.rb +481 -0
- data/lib/better_model/railtie.rb +4 -0
- data/lib/better_model/searchable.rb +524 -0
- data/lib/better_model/sortable.rb +217 -0
- data/lib/better_model/statusable.rb +154 -0
- data/lib/better_model/version.rb +3 -0
- data/lib/better_model.rb +24 -0
- data/lib/generators/better_model/archivable/archivable_generator.rb +55 -0
- data/lib/generators/better_model/archivable/templates/migration.rb.tt +26 -0
- data/lib/tasks/better_model_tasks.rake +4 -0
- metadata +82 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Searchable - Sistema di ricerca unificato per modelli Rails
|
|
4
|
+
#
|
|
5
|
+
# Questo concern orchestra Predicable e Sortable per fornire un'interfaccia
|
|
6
|
+
# di ricerca completa con filtri, ordinamento e paginazione.
|
|
7
|
+
#
|
|
8
|
+
# Esempio di utilizzo:
|
|
9
|
+
# class Article < ApplicationRecord
|
|
10
|
+
# include BetterModel
|
|
11
|
+
#
|
|
12
|
+
# predicates :title, :status, :view_count, :published_at
|
|
13
|
+
# sort :title, :view_count, :published_at
|
|
14
|
+
#
|
|
15
|
+
# searchable do
|
|
16
|
+
# per_page 25
|
|
17
|
+
# max_per_page 100
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Utilizzo:
|
|
22
|
+
# Article.search(
|
|
23
|
+
# { title_cont: "Rails", status_eq: "published" },
|
|
24
|
+
# pagination: { page: 1, per_page: 25 },
|
|
25
|
+
# orders: [:sort_published_at_desc, :sort_title_asc]
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
module BetterModel
|
|
29
|
+
module Searchable
|
|
30
|
+
extend ActiveSupport::Concern
|
|
31
|
+
|
|
32
|
+
# Error classes
|
|
33
|
+
class Error < StandardError; end
|
|
34
|
+
class InvalidPredicateError < Error; end
|
|
35
|
+
class InvalidOrderError < Error; end
|
|
36
|
+
class InvalidPaginationError < Error; end
|
|
37
|
+
class InvalidSecurityError < Error; end
|
|
38
|
+
|
|
39
|
+
included do
|
|
40
|
+
# Valida che sia incluso solo in modelli ActiveRecord
|
|
41
|
+
unless ancestors.include?(ActiveRecord::Base)
|
|
42
|
+
raise ArgumentError, "BetterModel::Searchable can only be included in ActiveRecord models"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Configuration registry
|
|
46
|
+
class_attribute :searchable_config, default: {
|
|
47
|
+
default_order: nil,
|
|
48
|
+
per_page: nil,
|
|
49
|
+
max_per_page: nil,
|
|
50
|
+
securities: {}
|
|
51
|
+
}.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class_methods do
|
|
55
|
+
# Interfaccia principale di ricerca
|
|
56
|
+
#
|
|
57
|
+
# @param predicates [Hash] Condizioni di filtro (usa scope Predicable)
|
|
58
|
+
# @option predicates [Array<Hash>] :or Condizioni OR
|
|
59
|
+
# @param pagination [Hash] Parametri di paginazione (opzionale)
|
|
60
|
+
# @option pagination [Integer] :page Numero pagina
|
|
61
|
+
# @option pagination [Integer] :per_page Risultati per pagina
|
|
62
|
+
# @param orders [Array<Symbol>] Array di symbol scope sortable (opzionale)
|
|
63
|
+
#
|
|
64
|
+
# @return [ActiveRecord::Relation] Relation concatenabile
|
|
65
|
+
#
|
|
66
|
+
# @example Ricerca base
|
|
67
|
+
# Article.search({ title_cont: "Rails" })
|
|
68
|
+
#
|
|
69
|
+
# @example Con paginazione e ordinamento
|
|
70
|
+
# Article.search(
|
|
71
|
+
# { title_cont: "Rails", status_eq: "published" },
|
|
72
|
+
# pagination: { page: 1, per_page: 25 },
|
|
73
|
+
# orders: [:sort_published_at_desc]
|
|
74
|
+
# )
|
|
75
|
+
#
|
|
76
|
+
# @example Con OR conditions
|
|
77
|
+
# Article.search(
|
|
78
|
+
# {
|
|
79
|
+
# or: [
|
|
80
|
+
# { title_cont: "Rails" },
|
|
81
|
+
# { title_cont: "Ruby" }
|
|
82
|
+
# ],
|
|
83
|
+
# status_eq: "published"
|
|
84
|
+
# },
|
|
85
|
+
# orders: [:sort_view_count_desc]
|
|
86
|
+
# )
|
|
87
|
+
#
|
|
88
|
+
def search(predicates = {}, **options)
|
|
89
|
+
# Extract and validate keyword arguments
|
|
90
|
+
pagination = options.delete(:pagination)
|
|
91
|
+
orders = options.delete(:orders)
|
|
92
|
+
security = options.delete(:security)
|
|
93
|
+
|
|
94
|
+
# If there are remaining unknown options, they might be misplaced predicates
|
|
95
|
+
if options.any?
|
|
96
|
+
raise ArgumentError, "Unknown keyword arguments: #{options.keys.join(', ')}. " \
|
|
97
|
+
"Did you mean to pass predicates as a hash? Use: search({#{options.keys.first}: ...})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sanitize predicates
|
|
101
|
+
predicates = sanitize_predicates(predicates)
|
|
102
|
+
|
|
103
|
+
# Extract OR conditions from predicates hash
|
|
104
|
+
or_conditions = predicates.delete(:or)
|
|
105
|
+
|
|
106
|
+
# Validate query complexity limits to prevent DoS attacks
|
|
107
|
+
validate_query_complexity(predicates, or_conditions)
|
|
108
|
+
|
|
109
|
+
# Validate security if specified
|
|
110
|
+
# Valida sia i predicati AND che quelli dentro le condizioni OR
|
|
111
|
+
if security.present?
|
|
112
|
+
validate_security(security, predicates)
|
|
113
|
+
validate_security_in_or_conditions(security, or_conditions) if or_conditions.present?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Start with base scope
|
|
117
|
+
scope = all
|
|
118
|
+
|
|
119
|
+
# Apply AND predicates
|
|
120
|
+
scope = apply_predicates(scope, predicates) if predicates.any?
|
|
121
|
+
|
|
122
|
+
# Apply OR conditions
|
|
123
|
+
scope = apply_or_conditions(scope, or_conditions) if or_conditions.present?
|
|
124
|
+
|
|
125
|
+
# Apply orders: use provided orders, or fall back to default_order
|
|
126
|
+
if orders.present?
|
|
127
|
+
scope = apply_orders(scope, orders)
|
|
128
|
+
elsif searchable_config[:default_order].present?
|
|
129
|
+
scope = apply_orders(scope, searchable_config[:default_order])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Apply pagination
|
|
133
|
+
scope = apply_pagination(scope, pagination) if pagination.present?
|
|
134
|
+
|
|
135
|
+
scope
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# DSL per configurare searchable
|
|
139
|
+
#
|
|
140
|
+
# @example
|
|
141
|
+
# searchable do
|
|
142
|
+
# per_page 25
|
|
143
|
+
# max_per_page 100
|
|
144
|
+
# end
|
|
145
|
+
def searchable(&block)
|
|
146
|
+
configurator = SearchableConfigurator.new(self)
|
|
147
|
+
configurator.instance_eval(&block)
|
|
148
|
+
self.searchable_config = configurator.to_h.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Verifica se un campo è predicable
|
|
152
|
+
def searchable_field?(field_name)
|
|
153
|
+
respond_to?(:predicable_field?) && predicable_field?(field_name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Ritorna tutti i campi predicable
|
|
157
|
+
def searchable_fields
|
|
158
|
+
respond_to?(:predicable_fields) ? predicable_fields : Set.new
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Ritorna i predicati disponibili per un campo
|
|
162
|
+
#
|
|
163
|
+
# @param field_name [Symbol] Nome del campo
|
|
164
|
+
# @return [Array<Symbol>] Array di predicati disponibili
|
|
165
|
+
#
|
|
166
|
+
# @example
|
|
167
|
+
# Article.searchable_predicates_for(:title)
|
|
168
|
+
# # => [:eq, :not_eq, :cont, :i_cont, :start, :end, ...]
|
|
169
|
+
def searchable_predicates_for(field_name)
|
|
170
|
+
return [] unless searchable_field?(field_name)
|
|
171
|
+
return [] unless respond_to?(:predicable_scopes)
|
|
172
|
+
|
|
173
|
+
predicable_scopes
|
|
174
|
+
.select { |scope| scope.to_s.start_with?("#{field_name}_") }
|
|
175
|
+
.map { |scope| scope.to_s.delete_prefix("#{field_name}_").to_sym }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Ritorna gli ordinamenti disponibili per un campo
|
|
179
|
+
#
|
|
180
|
+
# @param field_name [Symbol] Nome del campo
|
|
181
|
+
# @return [Array<Symbol>] Array di scope sortable disponibili
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# Article.searchable_sorts_for(:title)
|
|
185
|
+
# # => [:sort_title_asc, :sort_title_desc, :sort_title_asc_i, :sort_title_desc_i]
|
|
186
|
+
def searchable_sorts_for(field_name)
|
|
187
|
+
return [] unless respond_to?(:sortable_field?) && sortable_field?(field_name)
|
|
188
|
+
return [] unless respond_to?(:sortable_scopes)
|
|
189
|
+
|
|
190
|
+
sortable_scopes
|
|
191
|
+
.select { |scope| scope.to_s.start_with?("sort_#{field_name}_") }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
# Sanitizza e normalizza parametri predicates
|
|
197
|
+
# IMPORTANTE: Non usa più to_unsafe_h per sicurezza. Se passi ActionController::Parameters,
|
|
198
|
+
# devi chiamare .permit! o .to_h esplicitamente nel controller prima di passarlo qui.
|
|
199
|
+
def sanitize_predicates(predicates)
|
|
200
|
+
return {} if predicates.nil?
|
|
201
|
+
|
|
202
|
+
# Se è un ActionController::Parameters, converti a hash
|
|
203
|
+
# Ma richiedi che sia già stato permesso (non bypassa strong parameters)
|
|
204
|
+
if defined?(ActionController::Parameters) && predicates.is_a?(ActionController::Parameters)
|
|
205
|
+
unless predicates.permitted?
|
|
206
|
+
raise ArgumentError,
|
|
207
|
+
"ActionController::Parameters must be explicitly permitted before passing to search. " \
|
|
208
|
+
"Use .permit! or .to_h in your controller."
|
|
209
|
+
end
|
|
210
|
+
predicates = predicates.to_h
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
predicates.deep_symbolize_keys
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Applica predicati AND
|
|
217
|
+
def apply_predicates(scope, predicates)
|
|
218
|
+
predicates.each do |predicate_scope, value|
|
|
219
|
+
# Validate scope exists
|
|
220
|
+
unless respond_to?(:predicable_scope?) && predicable_scope?(predicate_scope)
|
|
221
|
+
raise InvalidPredicateError,
|
|
222
|
+
"Invalid predicate scope: #{predicate_scope}. " \
|
|
223
|
+
"Available predicable scopes: #{predicable_scopes.to_a.join(', ')}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Skip nil/blank values (but not false)
|
|
227
|
+
next if value.nil?
|
|
228
|
+
next if value.respond_to?(:empty?) && value.empty?
|
|
229
|
+
|
|
230
|
+
# Apply scope
|
|
231
|
+
scope = if value == true || value == "true"
|
|
232
|
+
scope.public_send(predicate_scope)
|
|
233
|
+
elsif value.is_a?(Array)
|
|
234
|
+
# Splat array values for predicates like _between that expect multiple args
|
|
235
|
+
scope.public_send(predicate_scope, *value)
|
|
236
|
+
else
|
|
237
|
+
scope.public_send(predicate_scope, value)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
scope
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Applica condizioni OR
|
|
245
|
+
def apply_or_conditions(scope, or_conditions_array)
|
|
246
|
+
# Build OR query usando Arel
|
|
247
|
+
or_relation = or_conditions_array.map do |condition_hash|
|
|
248
|
+
temp_scope = all
|
|
249
|
+
condition_hash.symbolize_keys.each do |predicate_scope, value|
|
|
250
|
+
unless respond_to?(:predicable_scope?) && predicable_scope?(predicate_scope)
|
|
251
|
+
raise InvalidPredicateError,
|
|
252
|
+
"Invalid predicate in OR condition: #{predicate_scope}. " \
|
|
253
|
+
"Available predicable scopes: #{predicable_scopes.to_a.join(', ')}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
temp_scope = if value == true || value == "true"
|
|
257
|
+
temp_scope.public_send(predicate_scope)
|
|
258
|
+
elsif value.is_a?(Array)
|
|
259
|
+
# Splat array values for predicates like _between that expect multiple args
|
|
260
|
+
temp_scope.public_send(predicate_scope, *value)
|
|
261
|
+
else
|
|
262
|
+
temp_scope.public_send(predicate_scope, value)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
temp_scope
|
|
266
|
+
end.reduce { |union, condition| union.or(condition) }
|
|
267
|
+
|
|
268
|
+
scope.merge(or_relation)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Applica ordinamenti
|
|
272
|
+
def apply_orders(scope, orders_array)
|
|
273
|
+
Array(orders_array).each do |order_scope|
|
|
274
|
+
order_scope = order_scope.to_sym
|
|
275
|
+
|
|
276
|
+
# Validate scope exists
|
|
277
|
+
unless respond_to?(:sortable_scope?) && sortable_scope?(order_scope)
|
|
278
|
+
raise InvalidOrderError,
|
|
279
|
+
"Invalid order scope: #{order_scope}. " \
|
|
280
|
+
"Available sortable scopes: #{sortable_scopes.to_a.join(', ')}"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Apply scope
|
|
284
|
+
scope = scope.public_send(order_scope)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
scope
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Applica paginazione
|
|
291
|
+
def apply_pagination(scope, pagination_params)
|
|
292
|
+
page = pagination_params[:page]&.to_i || 1
|
|
293
|
+
per_page = pagination_params[:per_page]&.to_i
|
|
294
|
+
|
|
295
|
+
# Validate page >= 1 (always, even without per_page)
|
|
296
|
+
raise InvalidPaginationError, "page must be >= 1" if page < 1
|
|
297
|
+
|
|
298
|
+
# DoS protection: limita il numero massimo di pagina per evitare offset enormi
|
|
299
|
+
# Default 10000, configurabile tramite searchable config
|
|
300
|
+
max_page = searchable_config[:max_page] || 10_000
|
|
301
|
+
if page > max_page
|
|
302
|
+
raise InvalidPaginationError,
|
|
303
|
+
"page must be <= #{max_page} (DoS protection). " \
|
|
304
|
+
"Configure max_page in searchable block to change this limit."
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# If per_page is not provided, return scope without LIMIT
|
|
308
|
+
return scope if per_page.nil?
|
|
309
|
+
|
|
310
|
+
# Validate per_page >= 1
|
|
311
|
+
raise InvalidPaginationError, "per_page must be >= 1" if per_page < 1
|
|
312
|
+
|
|
313
|
+
# Respect max_per_page limit if configured
|
|
314
|
+
if searchable_config[:max_per_page].present?
|
|
315
|
+
per_page = [ per_page, searchable_config[:max_per_page] ].min
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
offset = (page - 1) * per_page
|
|
319
|
+
scope.limit(per_page).offset(offset)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Valida che i predicati obbligatori della security siano presenti con valori validi
|
|
323
|
+
def validate_security(security_name, predicates)
|
|
324
|
+
# Converti security_name a symbol
|
|
325
|
+
security_name = security_name.to_sym
|
|
326
|
+
|
|
327
|
+
# Verifica che la security esista nella configurazione
|
|
328
|
+
securities_config = searchable_config[:securities] || {}
|
|
329
|
+
|
|
330
|
+
unless securities_config.key?(security_name)
|
|
331
|
+
raise InvalidSecurityError,
|
|
332
|
+
"Unknown security: #{security_name}. " \
|
|
333
|
+
"Available securities: #{securities_config.keys.join(', ')}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Ottieni i predicati obbligatori per questa security
|
|
337
|
+
required_predicates = securities_config[security_name]
|
|
338
|
+
|
|
339
|
+
# Verifica che tutti i predicati obbligatori siano presenti CON valori validi
|
|
340
|
+
missing_or_blank = required_predicates.reject do |pred|
|
|
341
|
+
value = predicates[pred]
|
|
342
|
+
# Il predicato deve esistere, non essere nil, e non essere empty
|
|
343
|
+
predicates.key?(pred) &&
|
|
344
|
+
!value.nil? &&
|
|
345
|
+
!(value.respond_to?(:empty?) && value.empty?)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
if missing_or_blank.any?
|
|
349
|
+
raise InvalidSecurityError,
|
|
350
|
+
"Security :#{security_name} requires the following predicates with valid values: #{missing_or_blank.join(', ')}. " \
|
|
351
|
+
"These predicates must be present and have non-blank values in the search parameters."
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Valida che le condizioni OR rispettino i requisiti di security
|
|
356
|
+
# Le condizioni OR non devono permettere di bypassare le regole di security
|
|
357
|
+
def validate_security_in_or_conditions(security_name, or_conditions_array)
|
|
358
|
+
return if or_conditions_array.blank?
|
|
359
|
+
|
|
360
|
+
# Verifica che la security esista nella configurazione
|
|
361
|
+
securities_config = searchable_config[:securities] || {}
|
|
362
|
+
|
|
363
|
+
unless securities_config.key?(security_name)
|
|
364
|
+
raise InvalidSecurityError,
|
|
365
|
+
"Unknown security: #{security_name}. " \
|
|
366
|
+
"Available securities: #{securities_config.keys.join(', ')}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Ottieni i predicati obbligatori per questa security
|
|
370
|
+
required_predicates = securities_config[security_name]
|
|
371
|
+
|
|
372
|
+
# Valida ogni condizione OR
|
|
373
|
+
or_conditions_array.each_with_index do |condition_hash, index|
|
|
374
|
+
condition_hash = condition_hash.deep_symbolize_keys
|
|
375
|
+
|
|
376
|
+
# Verifica che tutti i predicati obbligatori siano presenti in questa condizione OR
|
|
377
|
+
missing_or_blank = required_predicates.reject do |pred|
|
|
378
|
+
value = condition_hash[pred]
|
|
379
|
+
# Il predicato deve esistere, non essere nil, e non essere empty
|
|
380
|
+
condition_hash.key?(pred) &&
|
|
381
|
+
!value.nil? &&
|
|
382
|
+
!(value.respond_to?(:empty?) && value.empty?)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
if missing_or_blank.any?
|
|
386
|
+
raise InvalidSecurityError,
|
|
387
|
+
"Security :#{security_name} violation in OR condition ##{index + 1}. " \
|
|
388
|
+
"Each OR condition must include the following predicates with valid values: #{missing_or_blank.join(', ')}. " \
|
|
389
|
+
"This prevents bypassing security rules through OR logic."
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Valida la complessità della query per prevenire attacchi DoS
|
|
395
|
+
def validate_query_complexity(predicates, or_conditions)
|
|
396
|
+
# Limita il numero totale di predicati (default 100, configurabile)
|
|
397
|
+
max_predicates = searchable_config[:max_predicates] || 100
|
|
398
|
+
total_predicates = predicates.size
|
|
399
|
+
|
|
400
|
+
if total_predicates > max_predicates
|
|
401
|
+
raise ArgumentError,
|
|
402
|
+
"Query too complex: #{total_predicates} predicates exceeds maximum of #{max_predicates}. " \
|
|
403
|
+
"Configure max_predicates in searchable block to change this limit."
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Limita il numero di condizioni OR (default 50, configurabile)
|
|
407
|
+
return unless or_conditions.present?
|
|
408
|
+
|
|
409
|
+
max_or_conditions = searchable_config[:max_or_conditions] || 50
|
|
410
|
+
or_count = Array(or_conditions).size
|
|
411
|
+
|
|
412
|
+
if or_count > max_or_conditions
|
|
413
|
+
raise ArgumentError,
|
|
414
|
+
"Query too complex: #{or_count} OR conditions exceeds maximum of #{max_or_conditions}. " \
|
|
415
|
+
"Configure max_or_conditions in searchable block to change this limit."
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Conta anche i predicati dentro ogni condizione OR
|
|
419
|
+
or_predicates_count = Array(or_conditions).sum { |cond| cond.size }
|
|
420
|
+
total_with_or = total_predicates + or_predicates_count
|
|
421
|
+
|
|
422
|
+
if total_with_or > max_predicates
|
|
423
|
+
raise ArgumentError,
|
|
424
|
+
"Query too complex: #{total_with_or} total predicates (including OR conditions) exceeds maximum of #{max_predicates}. " \
|
|
425
|
+
"Configure max_predicates in searchable block to change this limit."
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Metodi di istanza
|
|
431
|
+
|
|
432
|
+
# Ritorna metadata di ricerca per questo record
|
|
433
|
+
#
|
|
434
|
+
# @return [Hash] Hash con informazioni sui campi ricercabili e ordinabili
|
|
435
|
+
#
|
|
436
|
+
# @example
|
|
437
|
+
# article.search_metadata
|
|
438
|
+
# # => {
|
|
439
|
+
# # searchable_fields: [:title, :status, ...],
|
|
440
|
+
# # sortable_fields: [:title, :view_count, ...],
|
|
441
|
+
# # available_predicates: { title: [:eq, :cont, ...], ... },
|
|
442
|
+
# # available_sorts: { title: [:sort_title_asc, ...], ... },
|
|
443
|
+
# # pagination: { per_page: 25, max_per_page: 100 }
|
|
444
|
+
# # }
|
|
445
|
+
def search_metadata
|
|
446
|
+
{
|
|
447
|
+
searchable_fields: self.class.searchable_fields.to_a,
|
|
448
|
+
sortable_fields: self.class.respond_to?(:sortable_fields) ? self.class.sortable_fields.to_a : [],
|
|
449
|
+
available_predicates: self.class.searchable_fields.each_with_object({}) do |field, hash|
|
|
450
|
+
hash[field] = self.class.searchable_predicates_for(field)
|
|
451
|
+
end,
|
|
452
|
+
available_sorts: (self.class.respond_to?(:sortable_fields) ? self.class.sortable_fields : []).each_with_object({}) do |field, hash|
|
|
453
|
+
hash[field] = self.class.searchable_sorts_for(field)
|
|
454
|
+
end,
|
|
455
|
+
pagination: {
|
|
456
|
+
per_page: self.class.searchable_config[:per_page],
|
|
457
|
+
max_per_page: self.class.searchable_config[:max_per_page]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Internal configurator for searchable DSL
|
|
464
|
+
class SearchableConfigurator
|
|
465
|
+
attr_reader :config
|
|
466
|
+
|
|
467
|
+
def initialize(model_class)
|
|
468
|
+
@model_class = model_class
|
|
469
|
+
@config = {
|
|
470
|
+
default_order: nil,
|
|
471
|
+
per_page: nil,
|
|
472
|
+
max_per_page: nil,
|
|
473
|
+
max_page: nil,
|
|
474
|
+
max_predicates: nil,
|
|
475
|
+
max_or_conditions: nil,
|
|
476
|
+
securities: {}
|
|
477
|
+
}
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def default_order(order_scopes)
|
|
481
|
+
order_scopes = Array(order_scopes)
|
|
482
|
+
@config[:default_order] = order_scopes.map(&:to_sym)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def per_page(count)
|
|
486
|
+
@config[:per_page] = count.to_i
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def max_per_page(count)
|
|
490
|
+
@config[:max_per_page] = count.to_i
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# DoS protection: limite massimo numero di pagina (default 10000)
|
|
494
|
+
def max_page(count)
|
|
495
|
+
@config[:max_page] = count.to_i
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# DoS protection: limite massimo numero di predicati totali (default 100)
|
|
499
|
+
def max_predicates(count)
|
|
500
|
+
@config[:max_predicates] = count.to_i
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# DoS protection: limite massimo numero di condizioni OR (default 50)
|
|
504
|
+
def max_or_conditions(count)
|
|
505
|
+
@config[:max_or_conditions] = count.to_i
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def security(name, predicates_array)
|
|
509
|
+
name = name.to_sym
|
|
510
|
+
predicates_array = Array(predicates_array).map(&:to_sym)
|
|
511
|
+
|
|
512
|
+
# Valida che i predicati richiesti siano simboli validi
|
|
513
|
+
if predicates_array.empty?
|
|
514
|
+
raise ArgumentError, "Security :#{name} must have at least one required predicate"
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
@config[:securities][name] = predicates_array
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def to_h
|
|
521
|
+
@config
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|