better_model 2.1.0 → 3.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -13
  3. data/lib/better_model/archivable.rb +203 -91
  4. data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
  5. data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
  6. data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
  7. data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
  8. data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
  9. data/lib/better_model/errors/better_model_error.rb +9 -0
  10. data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
  11. data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
  12. data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
  13. data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
  14. data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
  15. data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
  16. data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
  17. data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
  18. data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
  19. data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
  20. data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
  21. data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
  22. data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
  23. data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
  24. data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
  25. data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
  26. data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
  27. data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
  28. data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
  29. data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
  30. data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
  31. data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
  32. data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
  33. data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
  34. data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
  35. data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
  36. data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
  37. data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
  38. data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
  39. data/lib/better_model/models/state_transition.rb +122 -0
  40. data/lib/better_model/models/version.rb +68 -0
  41. data/lib/better_model/permissible.rb +103 -52
  42. data/lib/better_model/predicable.rb +114 -63
  43. data/lib/better_model/repositable/base_repository.rb +232 -0
  44. data/lib/better_model/repositable.rb +32 -0
  45. data/lib/better_model/searchable.rb +92 -92
  46. data/lib/better_model/sortable.rb +137 -41
  47. data/lib/better_model/stateable/configurator.rb +71 -53
  48. data/lib/better_model/stateable/guard.rb +35 -15
  49. data/lib/better_model/stateable/transition.rb +59 -30
  50. data/lib/better_model/stateable.rb +33 -15
  51. data/lib/better_model/statusable.rb +84 -52
  52. data/lib/better_model/taggable.rb +120 -75
  53. data/lib/better_model/traceable.rb +56 -48
  54. data/lib/better_model/validatable/configurator.rb +49 -172
  55. data/lib/better_model/validatable.rb +88 -113
  56. data/lib/better_model/version.rb +1 -1
  57. data/lib/better_model.rb +42 -5
  58. data/lib/generators/better_model/repository/repository_generator.rb +141 -0
  59. data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
  60. data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
  61. data/lib/generators/better_model/stateable/templates/README +1 -1
  62. metadata +44 -7
  63. data/lib/better_model/state_transition.rb +0 -106
  64. data/lib/better_model/stateable/errors.rb +0 -48
  65. data/lib/better_model/validatable/business_rule_validator.rb +0 -47
  66. data/lib/better_model/validatable/order_validator.rb +0 -77
  67. data/lib/better_model/version_record.rb +0 -66
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Repositable
5
+ # Base repository class for implementing the Repository Pattern with BetterModel.
6
+ #
7
+ # Repositories encapsulate data access logic and provide a clean interface for querying models.
8
+ # This base class integrates seamlessly with BetterModel's Searchable, Predicable, and Sortable concerns.
9
+ #
10
+ # @example Basic usage
11
+ # class ArticleRepository < BetterModel::Repositable::BaseRepository
12
+ # def model_class = Article
13
+ #
14
+ # def published
15
+ # search({ status_eq: "published" })
16
+ # end
17
+ #
18
+ # def recent(days: 7)
19
+ # search({ created_at_gteq: days.days.ago })
20
+ # end
21
+ # end
22
+ #
23
+ # repo = ArticleRepository.new
24
+ # articles = repo.search({ status_eq: "published" }, page: 1, per_page: 20)
25
+ # article = repo.search({ id_eq: 1 }, limit: 1)
26
+ #
27
+ # @example With BetterModel Searchable
28
+ # # If your model uses BetterModel::Searchable:
29
+ # class Article < ApplicationRecord
30
+ # include BetterModel
31
+ #
32
+ # predicates :title, :status, :view_count, :published_at
33
+ # sort :title, :view_count, :published_at
34
+ # end
35
+ #
36
+ # # The repository automatically uses the model's search() method
37
+ # repo = ArticleRepository.new
38
+ # repo.search({ title_cont: "Rails", view_count_gt: 100 })
39
+ #
40
+ class BaseRepository
41
+ attr_reader :model
42
+
43
+ # Initialize a new repository instance.
44
+ #
45
+ # @param model_class [Class] The ActiveRecord model class (optional if model_class method is defined)
46
+ def initialize(model_class = nil)
47
+ @model = model_class || self.class.instance_method(:model_class).bind(self).call
48
+ end
49
+
50
+ # Main search method with support for predicates, pagination, ordering, and limits.
51
+ #
52
+ # This method integrates with BetterModel's Searchable concern if available,
53
+ # otherwise falls back to standard ActiveRecord queries.
54
+ #
55
+ # @param predicates [Hash] Filter conditions using BetterModel predicates
56
+ # @param page [Integer] Page number for pagination (default: 1)
57
+ # @param per_page [Integer] Records per page (default: 20)
58
+ # @param includes [Array] Associations to eager load
59
+ # @param joins [Array] Associations to join
60
+ # @param order [String, Hash] SQL order clause
61
+ # @param order_scope [Hash] BetterModel sort scope (e.g., { field: :published_at, direction: :desc })
62
+ # @param limit [Integer, Symbol, nil] Result limit:
63
+ # - Integer (1): Returns single record (first)
64
+ # - Integer (2+): Returns limited relation
65
+ # - nil: Returns all results (no limit)
66
+ # - :default: Uses pagination (default behavior)
67
+ #
68
+ # @return [ActiveRecord::Relation, ActiveRecord::Base, nil] Query results
69
+ #
70
+ # @example Basic search with predicates
71
+ # search({ status_eq: "published", view_count_gt: 100 })
72
+ #
73
+ # @example With pagination
74
+ # search({ status_eq: "published" }, page: 2, per_page: 50)
75
+ #
76
+ # @example Single result
77
+ # search({ id_eq: 1 }, limit: 1)
78
+ #
79
+ # @example All results (no pagination)
80
+ # search({ status_eq: "published" }, limit: nil)
81
+ #
82
+ # @example With eager loading
83
+ # search({ status_eq: "published" }, includes: [:author, :comments])
84
+ #
85
+ # @example With ordering
86
+ # search({ status_eq: "published" }, order_scope: { field: :published_at, direction: :desc })
87
+ #
88
+ def search(predicates = {}, page: 1, per_page: 20, includes: [], joins: [], order: nil, order_scope: nil,
89
+ limit: :default)
90
+ # Remove nil values (keep false for boolean predicates)
91
+ cleaned_predicates = (predicates || {}).compact
92
+
93
+ # Validate predicates if model supports it
94
+ validate_predicates!(cleaned_predicates) if cleaned_predicates.present?
95
+
96
+ # Use BetterModel's search() method if available, otherwise use all
97
+ result = if @model.respond_to?(:search) && cleaned_predicates.present?
98
+ @model.search(cleaned_predicates)
99
+ else
100
+ @model.all
101
+ end
102
+
103
+ # Apply joins BEFORE includes (necessary for ORDER BY on joined tables)
104
+ result = result.joins(*joins) if joins.present?
105
+
106
+ # Apply includes if specified
107
+ result = result.includes(*includes) if includes.present?
108
+
109
+ # Apply ordering: order_scope has priority over order
110
+ if order_scope.present?
111
+ scope_name = build_scope_name(order_scope)
112
+ result = result.send(scope_name) if result.respond_to?(scope_name)
113
+ elsif order.present?
114
+ result = result.order(order)
115
+ end
116
+
117
+ # Apply limit if specified (has priority over pagination)
118
+ case limit
119
+ when 1
120
+ result.first
121
+ when (2..)
122
+ result.limit(limit)
123
+ when nil
124
+ # limit: nil explicitly means unlimited - return all results
125
+ result
126
+ when :default
127
+ # :default means use pagination (this is the default when no limit is specified)
128
+ paginate(result, page: page, per_page: per_page)
129
+ else
130
+ # Fallback to pagination for any other case
131
+ paginate(result, page: page, per_page: per_page)
132
+ end
133
+ end
134
+
135
+ # Standard CRUD methods delegated to the model
136
+ delegate :find, to: :@model
137
+ delegate :find_by, to: :@model
138
+ delegate :create, to: :@model
139
+ delegate :create!, to: :@model
140
+
141
+ # Build a new model instance with the given attributes.
142
+ #
143
+ # @param attributes [Hash] Model attributes
144
+ # @return [ActiveRecord::Base] New model instance (not persisted)
145
+ def build(attributes = {})
146
+ @model.new(attributes)
147
+ end
148
+
149
+ # Update a record by ID.
150
+ #
151
+ # @param id [Integer] Record ID
152
+ # @param attributes [Hash] Attributes to update
153
+ # @return [ActiveRecord::Base] Updated record
154
+ # @raise [ActiveRecord::RecordNotFound] If record not found
155
+ # @raise [ActiveRecord::RecordInvalid] If validation fails
156
+ def update(id, attributes)
157
+ record = find(id)
158
+ record.update!(attributes)
159
+ record
160
+ end
161
+
162
+ # Delete a record by ID.
163
+ #
164
+ # @param id [Integer] Record ID
165
+ # @return [ActiveRecord::Base] Deleted record
166
+ # @raise [ActiveRecord::RecordNotFound] If record not found
167
+ def delete(id)
168
+ @model.destroy(id)
169
+ end
170
+
171
+ # Base ActiveRecord methods delegated to the model
172
+ delegate :where, to: :@model
173
+ delegate :all, to: :@model
174
+ delegate :count, to: :@model
175
+ delegate :exists?, to: :@model
176
+
177
+ private
178
+
179
+ # Validate that predicates are supported by the model.
180
+ #
181
+ # If the model includes BetterModel::Predicable, validates against predicable_scope?.
182
+ # Otherwise, checks if the model responds to the predicate method.
183
+ #
184
+ # @param predicates [Hash] Predicates to validate
185
+ def validate_predicates!(predicates)
186
+ predicates.each_key do |predicate|
187
+ # Check if model supports predicable_scope? validation
188
+ if @model.respond_to?(:predicable_scope?)
189
+ next if @model.predicable_scope?(predicate)
190
+
191
+ available_list = @model.predicable_scopes.to_a.sort.join(", ")
192
+ Rails.logger.error "Invalid predicate '#{predicate}' for model #{@model.name}. " \
193
+ "Available: #{available_list}"
194
+ else
195
+ # Fallback to checking if method exists
196
+ next if @model.respond_to?(predicate)
197
+
198
+ available_list = available_predicates.join(", ")
199
+ Rails.logger.error "Invalid predicate '#{predicate}' for model #{@model.name}. " \
200
+ "Available: #{available_list}"
201
+ end
202
+ end
203
+ end
204
+
205
+ # Get available predicates for models without BetterModel::Predicable.
206
+ #
207
+ # @return [Array<Symbol>] List of available predicate methods
208
+ def available_predicates
209
+ @model.methods.grep(/_eq$|_cont$|_gteq$|_lteq$|_in$|_not_null$|_null$/).sort
210
+ end
211
+
212
+ # Centralized pagination using ActiveRecord offset/limit.
213
+ #
214
+ # @param relation [ActiveRecord::Relation] Relation to paginate
215
+ # @param page [Integer] Page number (1-indexed)
216
+ # @param per_page [Integer] Records per page
217
+ # @return [ActiveRecord::Relation] Paginated relation
218
+ def paginate(relation, page:, per_page:)
219
+ offset_value = (page.to_i - 1) * per_page.to_i
220
+ relation.offset(offset_value).limit(per_page)
221
+ end
222
+
223
+ # Build scope name for ordering (e.g., "sort_published_at_desc").
224
+ #
225
+ # @param order_scope [Hash] Hash with :field and :direction keys
226
+ # @return [String] Scope name
227
+ def build_scope_name(order_scope)
228
+ "sort_#{order_scope[:field]}_#{order_scope[:direction]}"
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "repositable/base_repository"
4
+
5
+ module BetterModel
6
+ # Repositable provides the Repository Pattern infrastructure for BetterModel.
7
+ #
8
+ # Unlike other BetterModel concerns, Repositable is not included directly in models.
9
+ # Instead, it provides a BaseRepository class that you can inherit from to create
10
+ # repository classes for your models.
11
+ #
12
+ # @example Creating a repository
13
+ # class ArticleRepository < BetterModel::Repositable::BaseRepository
14
+ # def model_class = Article
15
+ #
16
+ # def published
17
+ # search({ status_eq: "published" })
18
+ # end
19
+ # end
20
+ #
21
+ # @example Using a repository
22
+ # repo = ArticleRepository.new
23
+ # articles = repo.published
24
+ # article = repo.find(1)
25
+ #
26
+ # @see BetterModel::Repositable::BaseRepository
27
+ #
28
+ module Repositable
29
+ # Version of the Repositable module
30
+ VERSION = "1.0.0"
31
+ end
32
+ end
@@ -1,11 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Searchable - Sistema di ricerca unificato per modelli Rails
3
+ require_relative "errors/searchable/searchable_error"
4
+ require_relative "errors/searchable/invalid_predicate_error"
5
+ require_relative "errors/searchable/invalid_order_error"
6
+ require_relative "errors/searchable/invalid_pagination_error"
7
+ require_relative "errors/searchable/invalid_security_error"
8
+ require_relative "errors/searchable/configuration_error"
9
+
10
+ # Searchable - Unified search system for Rails models.
4
11
  #
5
- # Questo concern orchestra Predicable e Sortable per fornire un'interfaccia
6
- # di ricerca completa con filtri, ordinamento e paginazione.
12
+ # This concern orchestrates Predicable and Sortable to provide a complete
13
+ # search interface with filters, sorting, and pagination.
7
14
  #
8
- # Esempio di utilizzo:
15
+ # @example Basic Usage
9
16
  # class Article < ApplicationRecord
10
17
  # include BetterModel
11
18
  #
@@ -18,7 +25,7 @@
18
25
  # end
19
26
  # end
20
27
  #
21
- # Utilizzo:
28
+ # @example Search with Filters, Pagination, and Sorting
22
29
  # Article.search(
23
30
  # { title_cont: "Rails", status_eq: "published" },
24
31
  # pagination: { page: 1, per_page: 25 },
@@ -29,17 +36,11 @@ module BetterModel
29
36
  module Searchable
30
37
  extend ActiveSupport::Concern
31
38
 
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
 
39
40
  included do
40
- # Valida che sia incluso solo in modelli ActiveRecord
41
+ # Validate ActiveRecord inheritance
41
42
  unless ancestors.include?(ActiveRecord::Base)
42
- raise ArgumentError, "BetterModel::Searchable can only be included in ActiveRecord models"
43
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid configuration"
43
44
  end
44
45
 
45
46
  # Configuration registry
@@ -52,28 +53,35 @@ module BetterModel
52
53
  end
53
54
 
54
55
  class_methods do
55
- # Interfaccia principale di ricerca
56
+ # Main search interface.
56
57
  #
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)
58
+ # Provides a unified search interface combining predicates (filters),
59
+ # sorting, and pagination with support for OR conditions and security policies.
63
60
  #
64
- # @return [ActiveRecord::Relation] Relation concatenabile
61
+ # @param predicates [Hash] Filter conditions (uses Predicable scopes)
62
+ # @option predicates [Array<Hash>] :or OR conditions
63
+ # @param pagination [Hash] Pagination parameters (optional)
64
+ # @option pagination [Integer] :page Page number
65
+ # @option pagination [Integer] :per_page Results per page
66
+ # @param orders [Array<Symbol>] Array of sortable scope symbols (optional)
67
+ # @param security [Symbol] Security policy name (optional)
68
+ # @param includes [Array, Symbol] Associations to eager load with LEFT OUTER JOIN
69
+ # @param preload [Array, Symbol] Associations to preload with separate queries
70
+ # @param eager_load [Array, Symbol] Associations to eager load (forces LEFT OUTER JOIN)
65
71
  #
66
- # @example Ricerca base
72
+ # @return [ActiveRecord::Relation] Chainable relation
73
+ #
74
+ # @example Basic search
67
75
  # Article.search({ title_cont: "Rails" })
68
76
  #
69
- # @example Con paginazione e ordinamento
77
+ # @example With pagination and sorting
70
78
  # Article.search(
71
79
  # { title_cont: "Rails", status_eq: "published" },
72
80
  # pagination: { page: 1, per_page: 25 },
73
81
  # orders: [:sort_published_at_desc]
74
82
  # )
75
83
  #
76
- # @example Con OR conditions
84
+ # @example With OR conditions
77
85
  # Article.search(
78
86
  # {
79
87
  # or: [
@@ -96,8 +104,7 @@ module BetterModel
96
104
 
97
105
  # If there are remaining unknown options, they might be misplaced predicates
98
106
  if options.any?
99
- raise ArgumentError, "Unknown keyword arguments: #{options.keys.join(', ')}. " \
100
- "Did you mean to pass predicates as a hash? Use: search({#{options.keys.first}: ...})"
107
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid search options provided"
101
108
  end
102
109
 
103
110
  # Sanitize predicates
@@ -143,7 +150,9 @@ module BetterModel
143
150
  scope
144
151
  end
145
152
 
146
- # DSL per configurare searchable
153
+ # DSL to configure searchable.
154
+ #
155
+ # @yield [configurator] Configuration block
147
156
  #
148
157
  # @example
149
158
  # searchable do
@@ -156,20 +165,21 @@ module BetterModel
156
165
  self.searchable_config = configurator.to_h.freeze
157
166
  end
158
167
 
159
- # Verifica se un campo è predicable
160
- def searchable_field?(field_name)
161
- respond_to?(:predicable_field?) && predicable_field?(field_name)
162
- end
168
+ # Check if a field is searchable (predicable).
169
+ #
170
+ # @param field_name [Symbol] Field name to check
171
+ # @return [Boolean] true if field is searchable
172
+ def searchable_field?(field_name) = respond_to?(:predicable_field?) && predicable_field?(field_name)
163
173
 
164
- # Ritorna tutti i campi predicable
165
- def searchable_fields
166
- respond_to?(:predicable_fields) ? predicable_fields : Set.new
167
- end
174
+ # Returns all searchable (predicable) fields.
175
+ #
176
+ # @return [Set<Symbol>] Set of searchable field names
177
+ def searchable_fields = respond_to?(:predicable_fields) ? predicable_fields : Set.new
168
178
 
169
- # Ritorna i predicati disponibili per un campo
179
+ # Returns available predicates for a field.
170
180
  #
171
- # @param field_name [Symbol] Nome del campo
172
- # @return [Array<Symbol>] Array di predicati disponibili
181
+ # @param field_name [Symbol] Field name
182
+ # @return [Array<Symbol>] Array of available predicates
173
183
  #
174
184
  # @example
175
185
  # Article.searchable_predicates_for(:title)
@@ -183,10 +193,10 @@ module BetterModel
183
193
  .map { |scope| scope.to_s.delete_prefix("#{field_name}_").to_sym }
184
194
  end
185
195
 
186
- # Ritorna gli ordinamenti disponibili per un campo
196
+ # Returns available sorts for a field.
187
197
  #
188
- # @param field_name [Symbol] Nome del campo
189
- # @return [Array<Symbol>] Array di scope sortable disponibili
198
+ # @param field_name [Symbol] Field name
199
+ # @return [Array<Symbol>] Array of available sortable scopes
190
200
  #
191
201
  # @example
192
202
  # Article.searchable_sorts_for(:title)
@@ -201,9 +211,14 @@ module BetterModel
201
211
 
202
212
  private
203
213
 
204
- # Sanitizza e normalizza parametri predicates
205
- # IMPORTANTE: Non usa più to_unsafe_h per sicurezza. Se passi ActionController::Parameters,
206
- # devi chiamare .permit! o .to_h esplicitamente nel controller prima di passarlo qui.
214
+ # Sanitize and normalize predicate parameters.
215
+ #
216
+ # @param predicates [Hash, ActionController::Parameters] Predicates to sanitize
217
+ # @return [Hash] Sanitized predicates hash
218
+ # @api private
219
+ #
220
+ # @note Does not use to_unsafe_h for security. If passing ActionController::Parameters,
221
+ # you must call .permit! or .to_h explicitly in your controller before passing here.
207
222
  def sanitize_predicates(predicates)
208
223
  return {} if predicates.nil?
209
224
 
@@ -211,9 +226,7 @@ module BetterModel
211
226
  # Ma richiedi che sia già stato permesso (non bypassa strong parameters)
212
227
  if defined?(ActionController::Parameters) && predicates.is_a?(ActionController::Parameters)
213
228
  unless predicates.permitted?
214
- raise ArgumentError,
215
- "ActionController::Parameters must be explicitly permitted before passing to search. " \
216
- "Use .permit! or .to_h in your controller."
229
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid configuration"
217
230
  end
218
231
  predicates = predicates.to_h
219
232
  end
@@ -226,9 +239,7 @@ module BetterModel
226
239
  predicates.each do |predicate_scope, value|
227
240
  # Validate scope exists
228
241
  unless respond_to?(:predicable_scope?) && predicable_scope?(predicate_scope)
229
- raise InvalidPredicateError,
230
- "Invalid predicate scope: #{predicate_scope}. " \
231
- "Available predicable scopes: #{predicable_scopes.to_a.join(', ')}"
242
+ raise BetterModel::Errors::Searchable::InvalidPredicateError, "Invalid predicate"
232
243
  end
233
244
 
234
245
  # Skip nil/blank values (but not false)
@@ -257,13 +268,11 @@ module BetterModel
257
268
  temp_scope = all
258
269
  condition_hash.symbolize_keys.each do |predicate_scope, value|
259
270
  unless respond_to?(:predicable_scope?) && predicable_scope?(predicate_scope)
260
- raise InvalidPredicateError,
261
- "Invalid predicate in OR condition: #{predicate_scope}. " \
262
- "Available predicable scopes: #{predicable_scopes.to_a.join(', ')}"
271
+ raise BetterModel::Errors::Searchable::InvalidPredicateError, "Invalid predicate"
263
272
  end
264
273
 
265
274
  temp_scope = if value == true || value == "true"
266
- temp_scope.public_send(predicate_scope)
275
+ temp_scope.public_send(predicate_scope, true)
267
276
  elsif value.is_a?(Array)
268
277
  # Splat array values for predicates like _between that expect multiple args
269
278
  temp_scope.public_send(predicate_scope, *value)
@@ -284,9 +293,7 @@ module BetterModel
284
293
 
285
294
  # Validate scope exists
286
295
  unless respond_to?(:sortable_scope?) && sortable_scope?(order_scope)
287
- raise InvalidOrderError,
288
- "Invalid order scope: #{order_scope}. " \
289
- "Available sortable scopes: #{sortable_scopes.to_a.join(', ')}"
296
+ raise BetterModel::Errors::Searchable::InvalidOrderError, "Invalid order scope"
290
297
  end
291
298
 
292
299
  # Apply scope
@@ -302,26 +309,28 @@ module BetterModel
302
309
  per_page = pagination_params[:per_page]&.to_i
303
310
 
304
311
  # Validate page >= 1 (always, even without per_page)
305
- raise InvalidPaginationError, "page must be >= 1" if page < 1
312
+ if page < 1
313
+ raise BetterModel::Errors::Searchable::InvalidPaginationError, "Invalid pagination parameters"
314
+ end
306
315
 
307
316
  # DoS protection: limita il numero massimo di pagina per evitare offset enormi
308
317
  # Default 10000, configurabile tramite searchable config
309
318
  max_page = searchable_config[:max_page] || 10_000
310
319
  if page > max_page
311
- raise InvalidPaginationError,
312
- "page must be <= #{max_page} (DoS protection). " \
313
- "Configure max_page in searchable block to change this limit."
320
+ raise BetterModel::Errors::Searchable::InvalidPaginationError, "Page number exceeds maximum allowed"
314
321
  end
315
322
 
316
323
  # If per_page is not provided, return scope without LIMIT
317
324
  return scope if per_page.nil?
318
325
 
319
326
  # Validate per_page >= 1
320
- raise InvalidPaginationError, "per_page must be >= 1" if per_page < 1
327
+ if per_page < 1
328
+ raise BetterModel::Errors::Searchable::InvalidPaginationError, "Invalid pagination parameters"
329
+ end
321
330
 
322
- # Respect max_per_page limit if configured
323
- if searchable_config[:max_per_page].present?
324
- per_page = [ per_page, searchable_config[:max_per_page] ].min
331
+ # Respect max_per_page limit if configured - raise error if exceeded
332
+ if searchable_config[:max_per_page].present? && per_page > searchable_config[:max_per_page]
333
+ raise BetterModel::Errors::Searchable::InvalidPaginationError, "per_page must be <= #{searchable_config[:max_per_page]}"
325
334
  end
326
335
 
327
336
  offset = (page - 1) * per_page
@@ -355,9 +364,7 @@ module BetterModel
355
364
  securities_config = searchable_config[:securities] || {}
356
365
 
357
366
  unless securities_config.key?(security_name)
358
- raise InvalidSecurityError,
359
- "Unknown security: #{security_name}. " \
360
- "Available securities: #{securities_config.keys.join(', ')}"
367
+ raise BetterModel::Errors::Searchable::InvalidSecurityError, "Unknown security policy"
361
368
  end
362
369
 
363
370
  # Ottieni i predicati obbligatori per questa security
@@ -373,9 +380,7 @@ module BetterModel
373
380
  end
374
381
 
375
382
  if missing_or_blank.any?
376
- raise InvalidSecurityError,
377
- "Security :#{security_name} requires the following predicates with valid values: #{missing_or_blank.join(', ')}. " \
378
- "These predicates must be present and have non-blank values in the search parameters."
383
+ raise BetterModel::Errors::Searchable::InvalidSecurityError, "Required security predicates missing"
379
384
  end
380
385
  end
381
386
 
@@ -388,9 +393,7 @@ module BetterModel
388
393
  securities_config = searchable_config[:securities] || {}
389
394
 
390
395
  unless securities_config.key?(security_name)
391
- raise InvalidSecurityError,
392
- "Unknown security: #{security_name}. " \
393
- "Available securities: #{securities_config.keys.join(', ')}"
396
+ raise BetterModel::Errors::Searchable::InvalidSecurityError, "Unknown security policy"
394
397
  end
395
398
 
396
399
  # Ottieni i predicati obbligatori per questa security
@@ -410,10 +413,7 @@ module BetterModel
410
413
  end
411
414
 
412
415
  if missing_or_blank.any?
413
- raise InvalidSecurityError,
414
- "Security :#{security_name} violation in OR condition ##{index + 1}. " \
415
- "Each OR condition must include the following predicates with valid values: #{missing_or_blank.join(', ')}. " \
416
- "This prevents bypassing security rules through OR logic."
416
+ raise BetterModel::Errors::Searchable::InvalidSecurityError, "Required security predicates missing in OR condition"
417
417
  end
418
418
  end
419
419
  end
@@ -425,9 +425,7 @@ module BetterModel
425
425
  total_predicates = predicates.size
426
426
 
427
427
  if total_predicates > max_predicates
428
- raise ArgumentError,
429
- "Query too complex: #{total_predicates} predicates exceeds maximum of #{max_predicates}. " \
430
- "Configure max_predicates in searchable block to change this limit."
428
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid configuration"
431
429
  end
432
430
 
433
431
  # Limita il numero di condizioni OR (default 50, configurabile)
@@ -437,9 +435,7 @@ module BetterModel
437
435
  or_count = Array(or_conditions).size
438
436
 
439
437
  if or_count > max_or_conditions
440
- raise ArgumentError,
441
- "Query too complex: #{or_count} OR conditions exceeds maximum of #{max_or_conditions}. " \
442
- "Configure max_or_conditions in searchable block to change this limit."
438
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid configuration"
443
439
  end
444
440
 
445
441
  # Conta anche i predicati dentro ogni condizione OR
@@ -447,18 +443,16 @@ module BetterModel
447
443
  total_with_or = total_predicates + or_predicates_count
448
444
 
449
445
  if total_with_or > max_predicates
450
- raise ArgumentError,
451
- "Query too complex: #{total_with_or} total predicates (including OR conditions) exceeds maximum of #{max_predicates}. " \
452
- "Configure max_predicates in searchable block to change this limit."
446
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Query complexity exceeds maximum allowed predicates"
453
447
  end
454
448
  end
455
449
  end
456
450
 
457
- # Metodi di istanza
451
+ # Instance Methods
458
452
 
459
- # Ritorna metadata di ricerca per questo record
453
+ # Returns search metadata for this record.
460
454
  #
461
- # @return [Hash] Hash con informazioni sui campi ricercabili e ordinabili
455
+ # @return [Hash] Hash with information about searchable and sortable fields
462
456
  #
463
457
  # @example
464
458
  # article.search_metadata
@@ -532,13 +526,19 @@ module BetterModel
532
526
  @config[:max_or_conditions] = count.to_i
533
527
  end
534
528
 
535
- def security(name, predicates_array)
529
+ def security(name, predicates_array = nil)
536
530
  name = name.to_sym
531
+
532
+ # Check if predicates_array is provided
533
+ if predicates_array.nil?
534
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid configuration"
535
+ end
536
+
537
537
  predicates_array = Array(predicates_array).map(&:to_sym)
538
538
 
539
539
  # Valida che i predicati richiesti siano simboli validi
540
540
  if predicates_array.empty?
541
- raise ArgumentError, "Security :#{name} must have at least one required predicate"
541
+ raise BetterModel::Errors::Searchable::ConfigurationError, "Invalid configuration"
542
542
  end
543
543
 
544
544
  @config[:securities][name] = predicates_array