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.
- checksums.yaml +4 -4
- data/README.md +96 -13
- data/lib/better_model/archivable.rb +203 -91
- data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
- data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
- data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/better_model_error.rb +9 -0
- data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
- data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
- data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
- data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
- data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
- data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
- data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
- data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
- data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
- data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
- data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
- data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
- data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
- data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
- data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
- data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
- data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
- data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
- data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
- data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
- data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
- data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
- data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
- data/lib/better_model/models/state_transition.rb +122 -0
- data/lib/better_model/models/version.rb +68 -0
- data/lib/better_model/permissible.rb +103 -52
- data/lib/better_model/predicable.rb +114 -63
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +92 -92
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +71 -53
- data/lib/better_model/stateable/guard.rb +35 -15
- data/lib/better_model/stateable/transition.rb +59 -30
- data/lib/better_model/stateable.rb +33 -15
- data/lib/better_model/statusable.rb +84 -52
- data/lib/better_model/taggable.rb +120 -75
- data/lib/better_model/traceable.rb +56 -48
- data/lib/better_model/validatable/configurator.rb +49 -172
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -5
- data/lib/generators/better_model/repository/repository_generator.rb +141 -0
- data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
- data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
- data/lib/generators/better_model/stateable/templates/README +1 -1
- metadata +44 -7
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -48
- data/lib/better_model/validatable/business_rule_validator.rb +0 -47
- data/lib/better_model/validatable/order_validator.rb +0 -77
- data/lib/better_model/version_record.rb +0 -66
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/permissible/permissible_error"
|
|
4
|
+
require_relative "errors/permissible/configuration_error"
|
|
5
|
+
|
|
6
|
+
# Permissible - Declarative permissions system for Rails models.
|
|
4
7
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
8
|
+
# This concern enables defining permissions/capabilities on models using a simple,
|
|
9
|
+
# declarative DSL, similar to the Statusable pattern but for operations.
|
|
7
10
|
#
|
|
8
|
-
#
|
|
11
|
+
# @example Basic Usage
|
|
9
12
|
# class Article < ApplicationRecord
|
|
10
13
|
# include BetterModel::Permissible
|
|
11
14
|
#
|
|
@@ -15,7 +18,7 @@
|
|
|
15
18
|
# permit :archive, -> { is?(:published) && created_at < 1.year.ago }
|
|
16
19
|
# end
|
|
17
20
|
#
|
|
18
|
-
#
|
|
21
|
+
# @example Checking Permissions
|
|
19
22
|
# article.permit?(:delete) # => true/false
|
|
20
23
|
# article.permit_delete? # => true/false
|
|
21
24
|
# article.permit_edit? # => true/false
|
|
@@ -26,58 +29,85 @@ module BetterModel
|
|
|
26
29
|
extend ActiveSupport::Concern
|
|
27
30
|
|
|
28
31
|
included do
|
|
29
|
-
# Registry
|
|
32
|
+
# Registry of permissions defined for this class
|
|
30
33
|
class_attribute :permit_definitions
|
|
31
34
|
self.permit_definitions = {}
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
class_methods do
|
|
35
|
-
# DSL
|
|
38
|
+
# DSL to define permissions.
|
|
39
|
+
#
|
|
40
|
+
# Defines a permission check that can be evaluated against model instances.
|
|
41
|
+
# Automatically creates a convenience method permit_<permission_name>? for each permission.
|
|
36
42
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
43
|
+
# @param permission_name [Symbol, String] Permission identifier (e.g., :delete, :edit)
|
|
44
|
+
# @param condition_proc [Proc, nil] Lambda or proc that defines the condition
|
|
45
|
+
# @yield Alternative to condition_proc parameter
|
|
46
|
+
# @raise [BetterModel::Errors::Permissible::ConfigurationError] If parameters are invalid
|
|
41
47
|
#
|
|
42
|
-
#
|
|
48
|
+
# @example With lambda parameter
|
|
43
49
|
# permit :delete, -> { status != "published" }
|
|
44
|
-
#
|
|
50
|
+
#
|
|
51
|
+
# @example With block
|
|
52
|
+
# permit :edit do
|
|
53
|
+
# is?(:draft)
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# @example Complex condition
|
|
45
57
|
# permit :publish do
|
|
46
58
|
# is?(:draft) && valid?(:publication)
|
|
47
59
|
# end
|
|
48
60
|
def permit(permission_name, condition_proc = nil, &block)
|
|
49
|
-
#
|
|
50
|
-
|
|
61
|
+
# Validate parameters before converting
|
|
62
|
+
if permission_name.blank?
|
|
63
|
+
raise BetterModel::Errors::Permissible::ConfigurationError, "Permission name cannot be blank"
|
|
64
|
+
end
|
|
51
65
|
|
|
52
66
|
permission_name = permission_name.to_sym
|
|
53
67
|
condition = condition_proc || block
|
|
54
|
-
raise ArgumentError, "Condition proc or block is required" unless condition
|
|
55
|
-
raise ArgumentError, "Condition must respond to call" unless condition.respond_to?(:call)
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
unless condition
|
|
70
|
+
raise BetterModel::Errors::Permissible::ConfigurationError, "Condition proc or block is required"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
unless condition.respond_to?(:call)
|
|
74
|
+
raise BetterModel::Errors::Permissible::ConfigurationError, "Condition must respond to call"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Register permission in registry
|
|
58
78
|
self.permit_definitions = permit_definitions.merge(permission_name => condition.freeze).freeze
|
|
59
79
|
|
|
60
|
-
#
|
|
80
|
+
# Generate dynamic method permit_#{permission_name}?
|
|
61
81
|
define_permit_method(permission_name)
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
# List all permissions defined for this class.
|
|
85
|
+
#
|
|
86
|
+
# @return [Array<Symbol>] Array of defined permission names
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# Article.defined_permissions # => [:delete, :edit, :publish]
|
|
90
|
+
def defined_permissions = permit_definitions.keys
|
|
68
91
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
# Check if a permission is defined.
|
|
93
|
+
#
|
|
94
|
+
# @param permission_name [Symbol, String] Permission name to check
|
|
95
|
+
# @return [Boolean] true if permission is defined
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# Article.permission_defined?(:delete) # => true
|
|
99
|
+
def permission_defined?(permission_name) = permit_definitions.key?(permission_name.to_sym)
|
|
73
100
|
|
|
74
101
|
private
|
|
75
102
|
|
|
76
|
-
#
|
|
103
|
+
# Generate dynamic method permit_#{permission_name}? for each defined permission.
|
|
104
|
+
#
|
|
105
|
+
# @param permission_name [Symbol] Permission name
|
|
106
|
+
# @api private
|
|
77
107
|
def define_permit_method(permission_name)
|
|
78
108
|
method_name = "permit_#{permission_name}?"
|
|
79
109
|
|
|
80
|
-
#
|
|
110
|
+
# Avoid redefining methods if they already exist
|
|
81
111
|
return if method_defined?(method_name)
|
|
82
112
|
|
|
83
113
|
define_method(method_name) do
|
|
@@ -86,35 +116,33 @@ module BetterModel
|
|
|
86
116
|
end
|
|
87
117
|
end
|
|
88
118
|
|
|
89
|
-
#
|
|
119
|
+
# Generic method to check if a permission is granted.
|
|
90
120
|
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
121
|
+
# Evaluates the permission condition in the context of the model instance.
|
|
122
|
+
# Returns false if permission is not defined (secure by default).
|
|
93
123
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# - false se il permesso non è garantito o non è definito
|
|
124
|
+
# @param permission_name [Symbol, String] Permission name to check
|
|
125
|
+
# @return [Boolean] true if permission is granted, false otherwise
|
|
97
126
|
#
|
|
98
|
-
#
|
|
99
|
-
# article.permit?(:delete)
|
|
127
|
+
# @example
|
|
128
|
+
# article.permit?(:delete) # => true
|
|
100
129
|
def permit?(permission_name)
|
|
101
130
|
permission_name = permission_name.to_sym
|
|
102
131
|
condition = self.class.permit_definitions[permission_name]
|
|
103
132
|
|
|
104
|
-
#
|
|
133
|
+
# If permission is not defined, return false (secure by default)
|
|
105
134
|
return false unless condition
|
|
106
135
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
136
|
+
# Evaluate condition in context of model instance
|
|
137
|
+
# Errors propagate naturally - fail fast
|
|
109
138
|
instance_exec(&condition)
|
|
110
139
|
end
|
|
111
140
|
|
|
112
|
-
#
|
|
141
|
+
# Returns all available permissions for this instance with their values.
|
|
113
142
|
#
|
|
114
|
-
#
|
|
115
|
-
# - Hash con chiavi simbolo (permessi) e valori booleani (garantiti/negati)
|
|
143
|
+
# @return [Hash{Symbol => Boolean}] Hash with permission names and their granted status
|
|
116
144
|
#
|
|
117
|
-
#
|
|
145
|
+
# @example
|
|
118
146
|
# article.permissions
|
|
119
147
|
# # => { delete: true, edit: false, publish: false, archive: false }
|
|
120
148
|
def permissions
|
|
@@ -123,26 +151,49 @@ module BetterModel
|
|
|
123
151
|
end
|
|
124
152
|
end
|
|
125
153
|
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
# Check if instance has at least one granted permission.
|
|
155
|
+
#
|
|
156
|
+
# @return [Boolean] true if any permission is granted
|
|
157
|
+
#
|
|
158
|
+
# @example
|
|
159
|
+
# article.has_any_permission? # => true
|
|
160
|
+
def has_any_permission? = permissions.values.any?
|
|
130
161
|
|
|
131
|
-
#
|
|
162
|
+
# Check if instance has all specified permissions granted.
|
|
163
|
+
#
|
|
164
|
+
# @param permission_names [Array<Symbol>] Permission names to check
|
|
165
|
+
# @return [Boolean] true if all permissions are granted
|
|
166
|
+
#
|
|
167
|
+
# @example
|
|
168
|
+
# article.has_all_permissions?([:edit, :publish]) # => false
|
|
132
169
|
def has_all_permissions?(permission_names)
|
|
133
170
|
Array(permission_names).all? { |permission_name| permit?(permission_name) }
|
|
134
171
|
end
|
|
135
172
|
|
|
136
|
-
#
|
|
173
|
+
# Filter a list of permissions returning only granted ones.
|
|
174
|
+
#
|
|
175
|
+
# @param permission_names [Array<Symbol>] Permission names to filter
|
|
176
|
+
# @return [Array<Symbol>] Granted permissions
|
|
177
|
+
#
|
|
178
|
+
# @example
|
|
179
|
+
# article.granted_permissions([:edit, :delete, :publish]) # => [:edit]
|
|
137
180
|
def granted_permissions(permission_names)
|
|
138
181
|
Array(permission_names).select { |permission_name| permit?(permission_name) }
|
|
139
182
|
end
|
|
140
183
|
|
|
141
|
-
# Override
|
|
184
|
+
# Override as_json to automatically include permissions if requested.
|
|
185
|
+
#
|
|
186
|
+
# @param options [Hash] Options for as_json
|
|
187
|
+
# @option options [Boolean] :include_permissions Include permissions in JSON output
|
|
188
|
+
# @return [Hash] JSON representation
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# article.as_json(include_permissions: true)
|
|
192
|
+
# # => { ..., "permissions" => { "delete" => true, "edit" => false } }
|
|
142
193
|
def as_json(options = {})
|
|
143
194
|
result = super
|
|
144
195
|
|
|
145
|
-
# Include
|
|
196
|
+
# Include permissions if explicitly requested, converting symbol keys to strings
|
|
146
197
|
result["permissions"] = permissions.transform_keys(&:to_s) if options[:include_permissions]
|
|
147
198
|
|
|
148
199
|
result
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/predicable/predicable_error"
|
|
4
|
+
require_relative "errors/predicable/configuration_error"
|
|
5
|
+
|
|
6
|
+
# Predicable - Declarative filters/predicates system for Rails models.
|
|
4
7
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
8
|
+
# This concern enables defining search predicates on models using a simple, declarative DSL
|
|
9
|
+
# that automatically generates scopes based on column type.
|
|
7
10
|
#
|
|
8
|
-
#
|
|
11
|
+
# @example Basic Usage
|
|
9
12
|
# class Article < ApplicationRecord
|
|
10
13
|
# include BetterModel::Predicable
|
|
11
14
|
#
|
|
12
15
|
# predicates :title, :status, :view_count, :published_at, :featured
|
|
13
16
|
# end
|
|
14
17
|
#
|
|
15
|
-
#
|
|
18
|
+
# @example Generated Scopes
|
|
16
19
|
# Article.title_eq("Ruby on Rails") # WHERE title = 'Ruby on Rails'
|
|
17
20
|
# Article.title_i_cont("rails") # WHERE LOWER(title) LIKE '%rails%'
|
|
18
21
|
# Article.view_count_gt(100) # WHERE view_count > 100
|
|
@@ -25,38 +28,40 @@ module BetterModel
|
|
|
25
28
|
extend ActiveSupport::Concern
|
|
26
29
|
|
|
27
30
|
included do
|
|
28
|
-
#
|
|
31
|
+
# Validate ActiveRecord inheritance
|
|
29
32
|
unless ancestors.include?(ActiveRecord::Base)
|
|
30
|
-
raise
|
|
33
|
+
raise BetterModel::Errors::Predicable::ConfigurationError, "BetterModel::Predicable can only be included in ActiveRecord models"
|
|
31
34
|
end
|
|
32
35
|
|
|
33
|
-
# Registry
|
|
36
|
+
# Registry of predicable fields defined for this class
|
|
34
37
|
class_attribute :predicable_fields, default: Set.new
|
|
35
|
-
# Registry
|
|
38
|
+
# Registry of generated predicable scopes
|
|
36
39
|
class_attribute :predicable_scopes, default: Set.new
|
|
37
|
-
# Registry
|
|
40
|
+
# Registry of custom complex predicates
|
|
38
41
|
class_attribute :complex_predicates_registry, default: {}.freeze
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
class_methods do
|
|
42
|
-
# DSL
|
|
45
|
+
# DSL to define predicable fields.
|
|
43
46
|
#
|
|
44
|
-
#
|
|
47
|
+
# Automatically generates filter scopes based on column type:
|
|
45
48
|
# - String: _eq, _not_eq, _matches, _start, _end, _cont, _not_cont, _i_cont, _not_i_cont, _in, _not_in, _present(bool), _blank(bool), _null(bool)
|
|
46
49
|
# - Numeric: _eq, _not_eq, _lt, _lteq, _gt, _gteq, _between, _not_between, _in, _not_in, _present(bool)
|
|
47
50
|
# - Boolean: _eq, _not_eq, _present(bool)
|
|
48
51
|
# - Date: _eq, _not_eq, _lt, _lteq, _gt, _gteq, _between, _not_between, _in, _not_in, _within(duration), _blank(bool), _null(bool)
|
|
49
52
|
#
|
|
50
|
-
#
|
|
53
|
+
# @param field_names [Array<Symbol>] Field names to make predicable
|
|
54
|
+
#
|
|
55
|
+
# @note All predicates require explicit parameters. Use _eq(true)/_eq(false) for booleans.
|
|
51
56
|
#
|
|
52
|
-
#
|
|
57
|
+
# @example
|
|
53
58
|
# predicates :title, :view_count, :published_at, :featured
|
|
54
59
|
def predicates(*field_names)
|
|
55
60
|
field_names.each do |field_name|
|
|
56
61
|
validate_predicable_field!(field_name)
|
|
57
62
|
register_predicable_field(field_name)
|
|
58
63
|
|
|
59
|
-
# Auto-
|
|
64
|
+
# Auto-detect type and generate appropriate scopes
|
|
60
65
|
column = columns_hash[field_name.to_s]
|
|
61
66
|
next unless column
|
|
62
67
|
|
|
@@ -85,65 +90,88 @@ module BetterModel
|
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
#
|
|
93
|
+
# Register a custom complex predicate.
|
|
89
94
|
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
95
|
+
# Allows defining complex filters that combine multiple conditions
|
|
96
|
+
# or use custom logic not covered by standard predicates.
|
|
92
97
|
#
|
|
93
|
-
#
|
|
98
|
+
# @param name [Symbol] Predicate name
|
|
99
|
+
# @yield Predicate implementation block
|
|
100
|
+
# @raise [BetterModel::Errors::Predicable::ConfigurationError] If block is not provided
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
94
103
|
# register_complex_predicate :recent_popular do |days = 7, min_views = 100|
|
|
95
104
|
# where("published_at >= ? AND view_count >= ?", days.days.ago, min_views)
|
|
96
105
|
# end
|
|
97
106
|
#
|
|
98
107
|
# Article.recent_popular(7, 100)
|
|
99
108
|
def register_complex_predicate(name, &block)
|
|
100
|
-
|
|
109
|
+
unless block_given?
|
|
110
|
+
raise BetterModel::Errors::Predicable::ConfigurationError, "Block required for complex predicate"
|
|
111
|
+
end
|
|
101
112
|
|
|
102
|
-
#
|
|
113
|
+
# Register in registry
|
|
103
114
|
self.complex_predicates_registry = complex_predicates_registry.merge(name.to_sym => block).freeze
|
|
104
115
|
|
|
105
|
-
#
|
|
116
|
+
# Define scope
|
|
106
117
|
scope name, block
|
|
107
118
|
|
|
108
|
-
#
|
|
119
|
+
# Register scope
|
|
109
120
|
register_predicable_scopes(name)
|
|
110
121
|
end
|
|
111
122
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
# Check if a field has been registered as predicable.
|
|
124
|
+
#
|
|
125
|
+
# @param field_name [Symbol] Field name to check
|
|
126
|
+
# @return [Boolean] true if field is predicable
|
|
127
|
+
def predicable_field?(field_name) = predicable_fields.include?(field_name.to_sym)
|
|
116
128
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
# Check if a predicable scope has been generated.
|
|
130
|
+
#
|
|
131
|
+
# @param scope_name [Symbol] Scope name to check
|
|
132
|
+
# @return [Boolean] true if scope exists
|
|
133
|
+
def predicable_scope?(scope_name) = predicable_scopes.include?(scope_name.to_sym)
|
|
121
134
|
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
# Check if a complex predicate has been registered.
|
|
136
|
+
#
|
|
137
|
+
# @param name [Symbol] Predicate name to check
|
|
138
|
+
# @return [Boolean] true if complex predicate exists
|
|
139
|
+
def complex_predicate?(name) = complex_predicates_registry.key?(name.to_sym)
|
|
126
140
|
|
|
127
141
|
private
|
|
128
142
|
|
|
129
|
-
#
|
|
143
|
+
# Validate that field exists in table.
|
|
144
|
+
#
|
|
145
|
+
# @param field_name [Symbol] Field name to validate
|
|
146
|
+
# @raise [BetterModel::Errors::Predicable::ConfigurationError] If field doesn't exist
|
|
147
|
+
# @api private
|
|
130
148
|
def validate_predicable_field!(field_name)
|
|
131
149
|
unless column_names.include?(field_name.to_s)
|
|
132
|
-
raise
|
|
150
|
+
raise BetterModel::Errors::Predicable::ConfigurationError, "Invalid field name: #{field_name}. Field does not exist in #{table_name}"
|
|
133
151
|
end
|
|
134
152
|
end
|
|
135
153
|
|
|
136
|
-
#
|
|
154
|
+
# Register a field in predicable_fields registry.
|
|
155
|
+
#
|
|
156
|
+
# @param field_name [Symbol] Field name to register
|
|
157
|
+
# @api private
|
|
137
158
|
def register_predicable_field(field_name)
|
|
138
159
|
self.predicable_fields = (predicable_fields + [ field_name.to_sym ]).to_set.freeze
|
|
139
160
|
end
|
|
140
161
|
|
|
141
|
-
#
|
|
162
|
+
# Register scopes in predicable_scopes registry.
|
|
163
|
+
#
|
|
164
|
+
# @param scope_names [Array<Symbol>] Scope names to register
|
|
165
|
+
# @api private
|
|
142
166
|
def register_predicable_scopes(*scope_names)
|
|
143
167
|
self.predicable_scopes = (predicable_scopes + scope_names.map(&:to_sym)).to_set.freeze
|
|
144
168
|
end
|
|
145
169
|
|
|
146
|
-
#
|
|
170
|
+
# Generate base predicates: _eq, _not_eq, _present.
|
|
171
|
+
#
|
|
172
|
+
# @param field_name [Symbol] Field name
|
|
173
|
+
# @param column_type [Symbol, nil] Column type
|
|
174
|
+
# @api private
|
|
147
175
|
def define_base_predicates(field_name, column_type = nil)
|
|
148
176
|
table = arel_table
|
|
149
177
|
field = table[field_name]
|
|
@@ -167,9 +195,13 @@ module BetterModel
|
|
|
167
195
|
register_predicable_scopes(*scopes_to_register)
|
|
168
196
|
end
|
|
169
197
|
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
198
|
+
# Generate predicates for string fields (14 scopes).
|
|
199
|
+
#
|
|
200
|
+
# Base predicates (_eq, _not_eq) are defined separately.
|
|
201
|
+
# _present(bool), _blank(bool), _null(bool) handle presence with parameters.
|
|
202
|
+
#
|
|
203
|
+
# @param field_name [Symbol] Field name
|
|
204
|
+
# @api private
|
|
173
205
|
def define_string_predicates(field_name)
|
|
174
206
|
table = arel_table
|
|
175
207
|
field = table[field_name]
|
|
@@ -238,8 +270,12 @@ module BetterModel
|
|
|
238
270
|
)
|
|
239
271
|
end
|
|
240
272
|
|
|
241
|
-
#
|
|
242
|
-
#
|
|
273
|
+
# Generate predicates for numeric fields (11 scopes).
|
|
274
|
+
#
|
|
275
|
+
# Base predicates (_eq, _not_eq, _present(bool)) are defined separately.
|
|
276
|
+
#
|
|
277
|
+
# @param field_name [Symbol] Field name
|
|
278
|
+
# @api private
|
|
243
279
|
def define_numeric_predicates(field_name)
|
|
244
280
|
table = arel_table
|
|
245
281
|
field = table[field_name]
|
|
@@ -271,19 +307,26 @@ module BetterModel
|
|
|
271
307
|
)
|
|
272
308
|
end
|
|
273
309
|
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
310
|
+
# Generate predicates for boolean fields (0 scopes).
|
|
311
|
+
#
|
|
312
|
+
# Base predicates (_eq, _not_eq, _present) are defined separately.
|
|
313
|
+
# Use _eq(true) or _eq(false) for boolean filtering.
|
|
314
|
+
#
|
|
315
|
+
# @param field_name [Symbol] Field name
|
|
316
|
+
# @api private
|
|
277
317
|
def define_boolean_predicates(field_name)
|
|
278
318
|
# No additional scopes needed for boolean fields
|
|
279
319
|
# Use field_eq(true) or field_eq(false) instead
|
|
280
320
|
end
|
|
281
321
|
|
|
282
|
-
#
|
|
322
|
+
# Generate predicates for PostgreSQL array fields (3 scopes).
|
|
283
323
|
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
#
|
|
324
|
+
# @param field_name [Symbol] Field name
|
|
325
|
+
# @api private
|
|
326
|
+
#
|
|
327
|
+
# @note This method is not covered by automated tests because it requires
|
|
328
|
+
# PostgreSQL. Tests run on SQLite for performance.
|
|
329
|
+
# Test manually on PostgreSQL with: rails console RAILS_ENV=test
|
|
287
330
|
def define_postgresql_array_predicates(field_name)
|
|
288
331
|
return unless postgresql_adapter?
|
|
289
332
|
|
|
@@ -335,11 +378,14 @@ module BetterModel
|
|
|
335
378
|
)
|
|
336
379
|
end
|
|
337
380
|
|
|
338
|
-
#
|
|
381
|
+
# Generate predicates for PostgreSQL JSONB fields (4 scopes).
|
|
382
|
+
#
|
|
383
|
+
# @param field_name [Symbol] Field name
|
|
384
|
+
# @api private
|
|
339
385
|
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
386
|
+
# @note This method is not covered by automated tests because it requires
|
|
387
|
+
# PostgreSQL with JSONB support. Tests run on SQLite for performance.
|
|
388
|
+
# Test manually on PostgreSQL with: rails console RAILS_ENV=test
|
|
343
389
|
def define_postgresql_jsonb_predicates(field_name)
|
|
344
390
|
return unless postgresql_adapter?
|
|
345
391
|
|
|
@@ -388,9 +434,13 @@ module BetterModel
|
|
|
388
434
|
)
|
|
389
435
|
end
|
|
390
436
|
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
437
|
+
# Generate predicates for date/datetime fields (11 scopes).
|
|
438
|
+
#
|
|
439
|
+
# Base predicates (_eq, _not_eq, _present(bool)) are defined separately.
|
|
440
|
+
# Date convenience shortcuts removed except _within(duration).
|
|
441
|
+
#
|
|
442
|
+
# @param field_name [Symbol] Field name
|
|
443
|
+
# @api private
|
|
394
444
|
def define_date_predicates(field_name)
|
|
395
445
|
table = arel_table
|
|
396
446
|
field = table[field_name]
|
|
@@ -440,10 +490,11 @@ module BetterModel
|
|
|
440
490
|
)
|
|
441
491
|
end
|
|
442
492
|
|
|
443
|
-
#
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
493
|
+
# Check if database adapter is PostgreSQL.
|
|
494
|
+
#
|
|
495
|
+
# @return [Boolean] true if PostgreSQL adapter
|
|
496
|
+
# @api private
|
|
497
|
+
def postgresql_adapter? = connection.adapter_name.match?(/PostgreSQL/i)
|
|
447
498
|
end
|
|
448
499
|
end
|
|
449
500
|
end
|