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.
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Permissible - Sistema di permessi dichiarativi per modelli Rails
4
+ #
5
+ # Questo concern permette di definire permessi/capacità sui modelli utilizzando un DSL
6
+ # semplice e dichiarativo, simile al pattern Statusable ma per le operazioni.
7
+ #
8
+ # Esempio di utilizzo:
9
+ # class Article < ApplicationRecord
10
+ # include BetterModel::Permissible
11
+ #
12
+ # permit :delete, -> { status != "published" }
13
+ # permit :edit, -> { is?(:draft) || is?(:scheduled) }
14
+ # permit :publish, -> { is?(:draft) && valid?(:publication) }
15
+ # permit :archive, -> { is?(:published) && created_at < 1.year.ago }
16
+ # end
17
+ #
18
+ # Utilizzo:
19
+ # article.permit?(:delete) # => true/false
20
+ # article.permit_delete? # => true/false
21
+ # article.permit_edit? # => true/false
22
+ # article.permit_publish? # => true/false
23
+ #
24
+ module BetterModel
25
+ module Permissible
26
+ extend ActiveSupport::Concern
27
+
28
+ included do
29
+ # Registry dei permessi definiti per questa classe
30
+ class_attribute :permit_definitions
31
+ self.permit_definitions = {}
32
+ end
33
+
34
+ class_methods do
35
+ # DSL per definire permessi
36
+ #
37
+ # Parametri:
38
+ # - permission_name: simbolo che rappresenta il permesso (es. :delete, :edit)
39
+ # - condition_proc: lambda o proc che definisce la condizione
40
+ # - block: blocco alternativo alla condition_proc
41
+ #
42
+ # Esempi:
43
+ # permit :delete, -> { status != "published" }
44
+ # permit :edit, -> { is?(:draft) }
45
+ # permit :publish do
46
+ # is?(:draft) && valid?(:publication)
47
+ # end
48
+ def permit(permission_name, condition_proc = nil, &block)
49
+ # Valida i parametri prima di convertire
50
+ raise ArgumentError, "Permission name cannot be blank" if permission_name.blank?
51
+
52
+ permission_name = permission_name.to_sym
53
+ 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
+
57
+ # Registra il permesso nel registry
58
+ self.permit_definitions = permit_definitions.merge(permission_name => condition.freeze).freeze
59
+
60
+ # Genera il metodo dinamico permit_#{permission_name}?
61
+ define_permit_method(permission_name)
62
+ end
63
+
64
+ # Lista di tutti i permessi definiti per questa classe
65
+ def defined_permissions
66
+ permit_definitions.keys
67
+ end
68
+
69
+ # Verifica se un permesso è definito
70
+ def permission_defined?(permission_name)
71
+ permit_definitions.key?(permission_name.to_sym)
72
+ end
73
+
74
+ private
75
+
76
+ # Genera dinamicamente il metodo permit_#{permission_name}? per ogni permesso definito
77
+ def define_permit_method(permission_name)
78
+ method_name = "permit_#{permission_name}?"
79
+
80
+ # Evita di ridefinire metodi se già esistono
81
+ return if method_defined?(method_name)
82
+
83
+ define_method(method_name) do
84
+ permit?(permission_name)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Metodo generico per verificare se un permesso è garantito
90
+ #
91
+ # Parametri:
92
+ # - permission_name: simbolo del permesso da verificare
93
+ #
94
+ # Ritorna:
95
+ # - true se il permesso è garantito
96
+ # - false se il permesso non è garantito o non è definito
97
+ #
98
+ # Esempio:
99
+ # article.permit?(:delete)
100
+ def permit?(permission_name)
101
+ permission_name = permission_name.to_sym
102
+ condition = self.class.permit_definitions[permission_name]
103
+
104
+ # Se il permesso non è definito, ritorna false (secure by default)
105
+ return false unless condition
106
+
107
+ # Valuta la condizione nel contesto dell'istanza del modello
108
+ # Gli errori si propagano naturalmente - fail fast
109
+ instance_exec(&condition)
110
+ end
111
+
112
+ # Ritorna tutti i permessi disponibili per questa istanza con i loro valori
113
+ #
114
+ # Ritorna:
115
+ # - Hash con chiavi simbolo (permessi) e valori booleani (garantiti/negati)
116
+ #
117
+ # Esempio:
118
+ # article.permissions
119
+ # # => { delete: true, edit: false, publish: false, archive: false }
120
+ def permissions
121
+ self.class.permit_definitions.each_with_object({}) do |(permission_name, _condition), result|
122
+ result[permission_name] = permit?(permission_name)
123
+ end
124
+ end
125
+
126
+ # Verifica se l'istanza ha almeno un permesso garantito
127
+ def has_any_permission?
128
+ permissions.values.any?
129
+ end
130
+
131
+ # Verifica se l'istanza ha tutti i permessi specificati garantiti
132
+ def has_all_permissions?(permission_names)
133
+ Array(permission_names).all? { |permission_name| permit?(permission_name) }
134
+ end
135
+
136
+ # Filtra una lista di permessi restituendo solo quelli garantiti
137
+ def granted_permissions(permission_names)
138
+ Array(permission_names).select { |permission_name| permit?(permission_name) }
139
+ end
140
+
141
+ # Override di as_json per includere automaticamente i permessi se richiesto
142
+ def as_json(options = {})
143
+ result = super
144
+
145
+ # Include i permessi se esplicitamente richiesto, converting symbol keys to strings
146
+ result["permissions"] = permissions.transform_keys(&:to_s) if options[:include_permissions]
147
+
148
+ result
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,481 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Predicable - Sistema di filtri/predicati dichiarativo per modelli Rails
4
+ #
5
+ # Questo concern permette di definire predicati di ricerca sui modelli utilizzando un DSL
6
+ # semplice e dichiarativo che genera automaticamente scope basati sul tipo di colonna.
7
+ #
8
+ # Esempio di utilizzo:
9
+ # class Article < ApplicationRecord
10
+ # include BetterModel::Predicable
11
+ #
12
+ # predicates :title, :status, :view_count, :published_at, :featured
13
+ # end
14
+ #
15
+ # Utilizzo:
16
+ # Article.title_eq("Ruby on Rails") # WHERE title = 'Ruby on Rails'
17
+ # Article.title_i_cont("rails") # WHERE LOWER(title) LIKE '%rails%'
18
+ # Article.view_count_gt(100) # WHERE view_count > 100
19
+ # Article.published_at_lteq(Date.today) # WHERE published_at <= '2025-10-29'
20
+ # Article.featured_true # WHERE featured = TRUE
21
+ # Article.status_in(["draft", "published"]) # WHERE status IN ('draft', 'published')
22
+ #
23
+ module BetterModel
24
+ module Predicable
25
+ extend ActiveSupport::Concern
26
+
27
+ included do
28
+ # Valida che sia incluso solo in modelli ActiveRecord
29
+ unless ancestors.include?(ActiveRecord::Base)
30
+ raise ArgumentError, "BetterModel::Predicable can only be included in ActiveRecord models"
31
+ end
32
+
33
+ # Registry dei campi predicable definiti per questa classe
34
+ class_attribute :predicable_fields, default: Set.new
35
+ # Registry degli scope predicable generati
36
+ class_attribute :predicable_scopes, default: Set.new
37
+ # Registry dei predicati complessi custom
38
+ class_attribute :complex_predicates_registry, default: {}.freeze
39
+ end
40
+
41
+ class_methods do
42
+ # DSL per definire campi predicable
43
+ #
44
+ # Genera automaticamente scope di filtro basati sul tipo di colonna:
45
+ # - String: _eq, _not_eq, _matches, _start, _end, _cont, _not_cont, _i_cont, _not_i_cont, _in, _not_in, _present, _blank, _null
46
+ # - Numeric: _eq, _not_eq, _lt, _lteq, _gt, _gteq, _in, _not_in, _present
47
+ # - Boolean: _eq, _not_eq, _true, _false, _present
48
+ # - Date: _eq, _not_eq, _lt, _lteq, _gt, _gteq, _in, _not_in, _present, _blank, _null, _not_null
49
+ #
50
+ # Esempio:
51
+ # predicates :title, :view_count, :published_at, :featured
52
+ def predicates(*field_names)
53
+ field_names.each do |field_name|
54
+ validate_predicable_field!(field_name)
55
+ register_predicable_field(field_name)
56
+
57
+ # Auto-rileva tipo e genera scope appropriati
58
+ column = columns_hash[field_name.to_s]
59
+ next unless column
60
+
61
+ case column.type
62
+ when :string, :text
63
+ define_string_predicates(field_name)
64
+ when :integer, :decimal, :float, :bigint
65
+ define_numeric_predicates(field_name)
66
+ when :boolean
67
+ define_boolean_predicates(field_name)
68
+ when :date, :datetime, :time, :timestamp
69
+ define_date_predicates(field_name)
70
+ when :jsonb, :json
71
+ # JSONB/JSON: base predicates + PostgreSQL-specific
72
+ define_base_predicates(field_name)
73
+ define_postgresql_jsonb_predicates(field_name)
74
+ else
75
+ # Check for array columns (PostgreSQL)
76
+ if column.respond_to?(:array?) && column.array?
77
+ define_base_predicates(field_name)
78
+ define_postgresql_array_predicates(field_name)
79
+ else
80
+ # Default: genera solo predicati base
81
+ define_base_predicates(field_name)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # Registra un predicato complesso custom
88
+ #
89
+ # Permette di definire filtri complessi che combinano più condizioni
90
+ # o utilizzano logica custom non coperta dai predicati standard.
91
+ #
92
+ # Esempio:
93
+ # register_complex_predicate :recent_popular do |days = 7, min_views = 100|
94
+ # where("published_at >= ? AND view_count >= ?", days.days.ago, min_views)
95
+ # end
96
+ #
97
+ # Article.recent_popular(7, 100)
98
+ def register_complex_predicate(name, &block)
99
+ raise ArgumentError, "Block required for complex predicate" unless block_given?
100
+
101
+ # Registra nel registry
102
+ self.complex_predicates_registry = complex_predicates_registry.merge(name.to_sym => block).freeze
103
+
104
+ # Definisce lo scope
105
+ scope name, block
106
+
107
+ # Registra lo scope
108
+ register_predicable_scopes(name)
109
+ end
110
+
111
+ # Verifica se un campo è stato registrato come predicable
112
+ def predicable_field?(field_name)
113
+ predicable_fields.include?(field_name.to_sym)
114
+ end
115
+
116
+ # Verifica se uno scope predicable è stato generato
117
+ def predicable_scope?(scope_name)
118
+ predicable_scopes.include?(scope_name.to_sym)
119
+ end
120
+
121
+ # Verifica se un predicato complesso è stato registrato
122
+ def complex_predicate?(name)
123
+ complex_predicates_registry.key?(name.to_sym)
124
+ end
125
+
126
+ private
127
+
128
+ # Valida che il campo esista nella tabella
129
+ def validate_predicable_field!(field_name)
130
+ unless column_names.include?(field_name.to_s)
131
+ raise ArgumentError, "Invalid field name: #{field_name}. Field does not exist in #{table_name}"
132
+ end
133
+ end
134
+
135
+ # Registra un campo nel registry predicable_fields
136
+ def register_predicable_field(field_name)
137
+ self.predicable_fields = (predicable_fields + [ field_name.to_sym ]).to_set.freeze
138
+ end
139
+
140
+ # Registra scope nel registry predicable_scopes
141
+ def register_predicable_scopes(*scope_names)
142
+ self.predicable_scopes = (predicable_scopes + scope_names.map(&:to_sym)).to_set.freeze
143
+ end
144
+
145
+ # Genera predicati base: _eq, _not_eq, _present
146
+ def define_base_predicates(field_name)
147
+ table = arel_table
148
+ field = table[field_name]
149
+
150
+ # Equality
151
+ scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
152
+ scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
153
+
154
+ # Presence
155
+ scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
156
+
157
+ register_predicable_scopes(
158
+ :"#{field_name}_eq",
159
+ :"#{field_name}_not_eq",
160
+ :"#{field_name}_present"
161
+ )
162
+ end
163
+
164
+ # Genera predicati per campi stringa (14 scope)
165
+ def define_string_predicates(field_name)
166
+ table = arel_table
167
+ field = table[field_name]
168
+
169
+ # Comparison (2)
170
+ scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
171
+ scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
172
+
173
+ # Pattern matching (4)
174
+ scope :"#{field_name}_matches", ->(pattern) { where(field.matches(pattern)) }
175
+ scope :"#{field_name}_start", ->(prefix) {
176
+ sanitized = ActiveRecord::Base.sanitize_sql_like(prefix.to_s)
177
+ where(field.matches("#{sanitized}%"))
178
+ }
179
+ scope :"#{field_name}_end", ->(suffix) {
180
+ sanitized = ActiveRecord::Base.sanitize_sql_like(suffix.to_s)
181
+ where(field.matches("%#{sanitized}"))
182
+ }
183
+ scope :"#{field_name}_cont", ->(substring) {
184
+ sanitized = ActiveRecord::Base.sanitize_sql_like(substring.to_s)
185
+ where(field.matches("%#{sanitized}%"))
186
+ }
187
+
188
+ # Case-insensitive pattern matching (2)
189
+ scope :"#{field_name}_i_cont", ->(substring) {
190
+ sanitized = ActiveRecord::Base.sanitize_sql_like(substring.to_s.downcase)
191
+ where(Arel::Nodes::NamedFunction.new("LOWER", [ field ]).matches("%#{sanitized}%"))
192
+ }
193
+ scope :"#{field_name}_not_cont", ->(substring) {
194
+ sanitized = ActiveRecord::Base.sanitize_sql_like(substring.to_s)
195
+ where.not(field.matches("%#{sanitized}%"))
196
+ }
197
+ scope :"#{field_name}_not_i_cont", ->(substring) {
198
+ sanitized = ActiveRecord::Base.sanitize_sql_like(substring.to_s.downcase)
199
+ where.not(Arel::Nodes::NamedFunction.new("LOWER", [ field ]).matches("%#{sanitized}%"))
200
+ }
201
+
202
+ # Array operations (2)
203
+ scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
204
+ scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
205
+
206
+ # Presence (3)
207
+ scope :"#{field_name}_present", -> { where(field.not_eq(nil).and(field.not_eq(""))) }
208
+ scope :"#{field_name}_blank", -> { where(field.eq(nil).or(field.eq(""))) }
209
+ scope :"#{field_name}_null", -> { where(field.eq(nil)) }
210
+
211
+ register_predicable_scopes(
212
+ :"#{field_name}_eq",
213
+ :"#{field_name}_not_eq",
214
+ :"#{field_name}_matches",
215
+ :"#{field_name}_start",
216
+ :"#{field_name}_end",
217
+ :"#{field_name}_cont",
218
+ :"#{field_name}_i_cont",
219
+ :"#{field_name}_not_cont",
220
+ :"#{field_name}_not_i_cont",
221
+ :"#{field_name}_in",
222
+ :"#{field_name}_not_in",
223
+ :"#{field_name}_present",
224
+ :"#{field_name}_blank",
225
+ :"#{field_name}_null"
226
+ )
227
+ end
228
+
229
+ # Genera predicati per campi numerici (11 scope)
230
+ def define_numeric_predicates(field_name)
231
+ table = arel_table
232
+ field = table[field_name]
233
+
234
+ # Comparison (6)
235
+ scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
236
+ scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
237
+ scope :"#{field_name}_lt", ->(value) { where(field.lt(value)) }
238
+ scope :"#{field_name}_lteq", ->(value) { where(field.lteq(value)) }
239
+ scope :"#{field_name}_gt", ->(value) { where(field.gt(value)) }
240
+ scope :"#{field_name}_gteq", ->(value) { where(field.gteq(value)) }
241
+
242
+ # Range queries (2)
243
+ scope :"#{field_name}_between", ->(min, max) { where(field.between(min..max)) }
244
+ scope :"#{field_name}_not_between", ->(min, max) { where.not(field.between(min..max)) }
245
+
246
+ # Array operations (2)
247
+ scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
248
+ scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
249
+
250
+ # Presence (1)
251
+ scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
252
+
253
+ register_predicable_scopes(
254
+ :"#{field_name}_eq",
255
+ :"#{field_name}_not_eq",
256
+ :"#{field_name}_lt",
257
+ :"#{field_name}_lteq",
258
+ :"#{field_name}_gt",
259
+ :"#{field_name}_gteq",
260
+ :"#{field_name}_between",
261
+ :"#{field_name}_not_between",
262
+ :"#{field_name}_in",
263
+ :"#{field_name}_not_in",
264
+ :"#{field_name}_present"
265
+ )
266
+ end
267
+
268
+ # Genera predicati per campi booleani (5 scope)
269
+ def define_boolean_predicates(field_name)
270
+ table = arel_table
271
+ field = table[field_name]
272
+
273
+ # Comparison (2)
274
+ scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
275
+ scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
276
+
277
+ # Boolean shortcuts (2)
278
+ scope :"#{field_name}_true", -> { where(field.eq(true)) }
279
+ scope :"#{field_name}_false", -> { where(field.eq(false)) }
280
+
281
+ # Presence (1)
282
+ scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
283
+
284
+ register_predicable_scopes(
285
+ :"#{field_name}_eq",
286
+ :"#{field_name}_not_eq",
287
+ :"#{field_name}_true",
288
+ :"#{field_name}_false",
289
+ :"#{field_name}_present"
290
+ )
291
+ end
292
+
293
+ # Genera predicati per campi array PostgreSQL (3 scope)
294
+ def define_postgresql_array_predicates(field_name)
295
+ return unless postgresql_adapter?
296
+
297
+ table = arel_table
298
+ field = table[field_name]
299
+ column = columns_hash[field_name.to_s]
300
+
301
+ # Array overlap (&&)
302
+ # Usa Arel per generare SQL sicuro con parameter binding
303
+ scope :"#{field_name}_overlaps", ->(array) {
304
+ sanitized_array = Array(array).map { |v| connection.quote(v) }.join(",")
305
+ where(
306
+ Arel::Nodes::InfixOperation.new(
307
+ "&&",
308
+ field,
309
+ Arel::Nodes::SqlLiteral.new("ARRAY[#{sanitized_array}]::#{column.sql_type}")
310
+ )
311
+ )
312
+ }
313
+
314
+ # Array contains (@>)
315
+ scope :"#{field_name}_contains", ->(value) {
316
+ sanitized_array = Array(value).map { |v| connection.quote(v) }.join(",")
317
+ where(
318
+ Arel::Nodes::InfixOperation.new(
319
+ "@>",
320
+ field,
321
+ Arel::Nodes::SqlLiteral.new("ARRAY[#{sanitized_array}]::#{column.sql_type}")
322
+ )
323
+ )
324
+ }
325
+
326
+ # Array contained by (<@)
327
+ scope :"#{field_name}_contained_by", ->(array) {
328
+ sanitized_array = Array(array).map { |v| connection.quote(v) }.join(",")
329
+ where(
330
+ Arel::Nodes::InfixOperation.new(
331
+ "<@",
332
+ field,
333
+ Arel::Nodes::SqlLiteral.new("ARRAY[#{sanitized_array}]::#{column.sql_type}")
334
+ )
335
+ )
336
+ }
337
+
338
+ register_predicable_scopes(
339
+ :"#{field_name}_overlaps",
340
+ :"#{field_name}_contains",
341
+ :"#{field_name}_contained_by"
342
+ )
343
+ end
344
+
345
+ # Genera predicati per campi JSONB PostgreSQL (4 scope)
346
+ def define_postgresql_jsonb_predicates(field_name)
347
+ return unless postgresql_adapter?
348
+
349
+ table = arel_table
350
+ field = table[field_name]
351
+ quoted_table = connection.quote_table_name(table_name)
352
+ quoted_field = connection.quote_column_name(field_name)
353
+
354
+ # JSONB has key (?)
355
+ # Usa named bind per evitare SQL injection
356
+ scope :"#{field_name}_has_key", ->(key) {
357
+ where("#{quoted_table}.#{quoted_field} ? :key", key: connection.quote(key.to_s))
358
+ }
359
+
360
+ # JSONB has any key (?|)
361
+ # Sanifica ogni chiave nell'array prima di passare alla query
362
+ scope :"#{field_name}_has_any_key", ->(keys) {
363
+ sanitized_keys = Array(keys).map { |k| connection.quote(k.to_s) }.join(",")
364
+ where("#{quoted_table}.#{quoted_field} ?| ARRAY[#{sanitized_keys}]")
365
+ }
366
+
367
+ # JSONB has all keys (?&)
368
+ scope :"#{field_name}_has_all_keys", ->(keys) {
369
+ sanitized_keys = Array(keys).map { |k| connection.quote(k.to_s) }.join(",")
370
+ where("#{quoted_table}.#{quoted_field} ?& ARRAY[#{sanitized_keys}]")
371
+ }
372
+
373
+ # JSONB contains (@>)
374
+ # Usa Arel per operazione @> con proper escaping
375
+ scope :"#{field_name}_jsonb_contains", ->(hash_or_value) {
376
+ json_value = hash_or_value.is_a?(String) ? hash_or_value : hash_or_value.to_json
377
+ where(
378
+ Arel::Nodes::InfixOperation.new(
379
+ "@>",
380
+ field,
381
+ Arel::Nodes::Quoted.new(json_value)
382
+ )
383
+ )
384
+ }
385
+
386
+ register_predicable_scopes(
387
+ :"#{field_name}_has_key",
388
+ :"#{field_name}_has_any_key",
389
+ :"#{field_name}_has_all_keys",
390
+ :"#{field_name}_jsonb_contains"
391
+ )
392
+ end
393
+
394
+ # Genera predicati per campi data/datetime (22 scope)
395
+ def define_date_predicates(field_name)
396
+ table = arel_table
397
+ field = table[field_name]
398
+
399
+ # Comparison (6)
400
+ scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
401
+ scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
402
+ scope :"#{field_name}_lt", ->(value) { where(field.lt(value)) }
403
+ scope :"#{field_name}_lteq", ->(value) { where(field.lteq(value)) }
404
+ scope :"#{field_name}_gt", ->(value) { where(field.gt(value)) }
405
+ scope :"#{field_name}_gteq", ->(value) { where(field.gteq(value)) }
406
+
407
+ # Range queries (2)
408
+ scope :"#{field_name}_between", ->(min, max) { where(field.between(min..max)) }
409
+ scope :"#{field_name}_not_between", ->(min, max) { where.not(field.between(min..max)) }
410
+
411
+ # Array operations (2)
412
+ scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
413
+ scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
414
+
415
+ # Date convenience shortcuts (8)
416
+ scope :"#{field_name}_today", -> {
417
+ where(field.between(Date.current.beginning_of_day..Date.current.end_of_day))
418
+ }
419
+ scope :"#{field_name}_yesterday", -> {
420
+ where(field.between(1.day.ago.beginning_of_day..1.day.ago.end_of_day))
421
+ }
422
+ scope :"#{field_name}_this_week", -> {
423
+ where(field.gteq(Date.current.beginning_of_week))
424
+ }
425
+ scope :"#{field_name}_this_month", -> {
426
+ where(field.gteq(Date.current.beginning_of_month))
427
+ }
428
+ scope :"#{field_name}_this_year", -> {
429
+ where(field.gteq(Date.current.beginning_of_year))
430
+ }
431
+ scope :"#{field_name}_past", -> {
432
+ where(field.lt(Time.current))
433
+ }
434
+ scope :"#{field_name}_future", -> {
435
+ where(field.gt(Time.current))
436
+ }
437
+ scope :"#{field_name}_within", ->(duration) {
438
+ # Auto-detect: ActiveSupport::Duration or numeric (days)
439
+ time_ago = duration.respond_to?(:ago) ? duration.ago : duration.to_i.days.ago
440
+ where(field.gteq(time_ago))
441
+ }
442
+
443
+ # Presence (4)
444
+ scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
445
+ scope :"#{field_name}_blank", -> { where(field.eq(nil)) }
446
+ scope :"#{field_name}_null", -> { where(field.eq(nil)) }
447
+ scope :"#{field_name}_not_null", -> { where(field.not_eq(nil)) }
448
+
449
+ register_predicable_scopes(
450
+ :"#{field_name}_eq",
451
+ :"#{field_name}_not_eq",
452
+ :"#{field_name}_lt",
453
+ :"#{field_name}_lteq",
454
+ :"#{field_name}_gt",
455
+ :"#{field_name}_gteq",
456
+ :"#{field_name}_between",
457
+ :"#{field_name}_not_between",
458
+ :"#{field_name}_in",
459
+ :"#{field_name}_not_in",
460
+ :"#{field_name}_today",
461
+ :"#{field_name}_yesterday",
462
+ :"#{field_name}_this_week",
463
+ :"#{field_name}_this_month",
464
+ :"#{field_name}_this_year",
465
+ :"#{field_name}_past",
466
+ :"#{field_name}_future",
467
+ :"#{field_name}_within",
468
+ :"#{field_name}_present",
469
+ :"#{field_name}_blank",
470
+ :"#{field_name}_null",
471
+ :"#{field_name}_not_null"
472
+ )
473
+ end
474
+
475
+ # Verifica se il database adapter è PostgreSQL
476
+ def postgresql_adapter?
477
+ connection.adapter_name.match?(/PostgreSQL/i)
478
+ end
479
+ end
480
+ end
481
+ end
@@ -0,0 +1,4 @@
1
+ module BetterModel
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end