better_model 1.1.0 β 1.2.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 +111 -1
- data/lib/better_model/predicable.rb +21 -11
- data/lib/better_model/sortable.rb +4 -0
- data/lib/better_model/stateable.rb +22 -8
- data/lib/better_model/traceable.rb +76 -24
- data/lib/better_model/validatable.rb +3 -3
- data/lib/better_model/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec236d26e4c1e0a5a62e9932606a1baa28de1bcadf8b447f1da545fb35d029b6
|
|
4
|
+
data.tar.gz: 24e4aa004924ab09c6104cad770229041b8c8d24556ce5f2f2d190bf15071e90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77d291e54bcbbb179eabd842290530d0f42e03636981e93bfdeb6575d5d0e02960d359072c0b0db0144f35cdbea647d463bf7320037c557552f6fce8365a393e
|
|
7
|
+
data.tar.gz: bfe3bf61206cb343dce4d84b2ff643e719c09b93a623de77e127a00aadbce646d25bfcc2cd66ca3bd725b6af224a82eb7cff2e059f79f45c8e026d5d878c95d6
|
data/README.md
CHANGED
|
@@ -91,7 +91,13 @@ class Article < ApplicationRecord
|
|
|
91
91
|
transition :archive, from: [:draft, :published], to: :archived
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
# 8.
|
|
94
|
+
# 8. TRACEABLE - Audit trail with time-travel (opt-in)
|
|
95
|
+
traceable do
|
|
96
|
+
track :title, :content, :status, :published_at
|
|
97
|
+
versions_table :article_versions # Optional: custom table
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 9. SEARCHABLE - Configure unified search interface
|
|
95
101
|
searchable do
|
|
96
102
|
per_page 25
|
|
97
103
|
max_per_page 100
|
|
@@ -149,6 +155,17 @@ article.published? # => true
|
|
|
149
155
|
article.state_transitions # History of all transitions
|
|
150
156
|
article.transition_history # Formatted history array
|
|
151
157
|
|
|
158
|
+
# β° Time travel & rollback (Traceable)
|
|
159
|
+
article.audit_trail # Full change history
|
|
160
|
+
article.as_of(3.days.ago) # Reconstruct past state
|
|
161
|
+
article.rollback_to(version) # Restore to previous version
|
|
162
|
+
article.changes_for(:status) # Changes for specific field
|
|
163
|
+
|
|
164
|
+
# π Query changes
|
|
165
|
+
Article.changed_by(user.id)
|
|
166
|
+
Article.changed_between(1.week.ago, Time.current)
|
|
167
|
+
Article.status_changed_from("draft").to("published")
|
|
168
|
+
|
|
152
169
|
# π Unified search with filters, sorting, and pagination
|
|
153
170
|
Article.search(
|
|
154
171
|
{ status_eq: "published", view_count_gteq: 50 },
|
|
@@ -166,6 +183,7 @@ class Article < ApplicationRecord
|
|
|
166
183
|
include BetterModel::Statusable # Only status management
|
|
167
184
|
include BetterModel::Permissible # Only permissions
|
|
168
185
|
include BetterModel::Archivable # Only archiving
|
|
186
|
+
include BetterModel::Traceable # Only audit trail & time-travel
|
|
169
187
|
include BetterModel::Sortable # Only sorting
|
|
170
188
|
include BetterModel::Predicable # Only filtering
|
|
171
189
|
include BetterModel::Validatable # Only validations
|
|
@@ -176,6 +194,24 @@ class Article < ApplicationRecord
|
|
|
176
194
|
end
|
|
177
195
|
```
|
|
178
196
|
|
|
197
|
+
## π Features Overview
|
|
198
|
+
|
|
199
|
+
BetterModel provides nine powerful concerns that work seamlessly together:
|
|
200
|
+
|
|
201
|
+
### Core Features
|
|
202
|
+
|
|
203
|
+
- **β¨ Statusable** - Declarative status management with lambda-based conditions
|
|
204
|
+
- **π Permissible** - State-based permission system
|
|
205
|
+
- **ποΈ Archivable** - Soft delete with tracking (by user, reason)
|
|
206
|
+
- **β° Traceable** π - Complete audit trail with time-travel and rollback
|
|
207
|
+
- **β¬οΈ Sortable** - Type-aware sorting scopes
|
|
208
|
+
- **π Predicable** - Advanced filtering with rich predicate system
|
|
209
|
+
- **π Searchable** - Unified search interface (Predicable + Sortable)
|
|
210
|
+
- **β
Validatable** - Declarative validation DSL with conditional rules
|
|
211
|
+
- **π Stateable** π - Declarative state machines with guards & callbacks
|
|
212
|
+
|
|
213
|
+
[See all features in detail β](#-features)
|
|
214
|
+
|
|
179
215
|
## βοΈ Requirements
|
|
180
216
|
|
|
181
217
|
- **Ruby:** 3.0 or higher
|
|
@@ -286,6 +322,27 @@ Define state machines declaratively with transitions, guards, validations, and c
|
|
|
286
322
|
|
|
287
323
|
---
|
|
288
324
|
|
|
325
|
+
### β° Traceable - Audit Trail with Time-Travel
|
|
326
|
+
|
|
327
|
+
Track all changes to your records with complete audit trail, time-travel capabilities, and rollback support.
|
|
328
|
+
|
|
329
|
+
**π― Key Benefits:**
|
|
330
|
+
- ποΈ Opt-in activation: only enabled when explicitly configured
|
|
331
|
+
- π Automatic change tracking on create, update, and destroy
|
|
332
|
+
- π€ User attribution: track who made each change
|
|
333
|
+
- π¬ Change reasons: optional context for changes
|
|
334
|
+
- β° Time-travel: reconstruct object state at any point in history
|
|
335
|
+
- β©οΈ Rollback support: restore records to previous versions
|
|
336
|
+
- π Rich query API: find changes by user, time, or field transitions
|
|
337
|
+
- π Flexible table naming: per-model, shared, or custom tables
|
|
338
|
+
- π Polymorphic association for efficient storage
|
|
339
|
+
- πΎ Database adapter safety: PostgreSQL, MySQL, SQLite support
|
|
340
|
+
- π Thread-safe dynamic class creation
|
|
341
|
+
|
|
342
|
+
**[π Full Documentation β](docs/traceable.md)**
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
289
346
|
### β¬οΈ Sortable - Type-Aware Sorting Scopes
|
|
290
347
|
|
|
291
348
|
Generate intelligent sorting scopes automatically with database-specific optimizations and NULL handling.
|
|
@@ -487,6 +544,38 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
|
|
|
487
544
|
- π» **Source Code:** [GitHub Repository](https://github.com/alessiobussolari/better_model)
|
|
488
545
|
- π **Documentation:** This README and detailed docs in `docs/` directory
|
|
489
546
|
|
|
547
|
+
## π Complete Documentation
|
|
548
|
+
|
|
549
|
+
### π Feature Guides
|
|
550
|
+
|
|
551
|
+
Detailed documentation for each BetterModel concern:
|
|
552
|
+
|
|
553
|
+
- [**Statusable**](docs/statusable.md) - Status management with derived conditions
|
|
554
|
+
- [**Permissible**](docs/permissible.md) - Permission system based on state
|
|
555
|
+
- [**Archivable**](docs/archivable.md) - Soft delete with comprehensive tracking
|
|
556
|
+
- [**Traceable**](docs/traceable.md) π - Audit trail, time-travel, and rollback
|
|
557
|
+
- [**Sortable**](docs/sortable.md) - Type-aware sorting system
|
|
558
|
+
- [**Predicable**](docs/predicable.md) - Advanced filtering and predicates
|
|
559
|
+
- [**Searchable**](docs/searchable.md) - Unified search interface
|
|
560
|
+
- [**Validatable**](docs/validatable.md) - Declarative validation system
|
|
561
|
+
- [**Stateable**](docs/stateable.md) π - State machine with transitions
|
|
562
|
+
|
|
563
|
+
### π Advanced Guides
|
|
564
|
+
|
|
565
|
+
Learn how to master BetterModel in production:
|
|
566
|
+
|
|
567
|
+
- [**Integration Guide**](docs/integration_guide.md) π - Combining multiple concerns effectively
|
|
568
|
+
- [**Performance Guide**](docs/performance_guide.md) π - Optimization strategies and indexing
|
|
569
|
+
- [**Migration Guide**](docs/migration_guide.md) π - Adding BetterModel to existing apps
|
|
570
|
+
|
|
571
|
+
### π‘ Quick Links
|
|
572
|
+
|
|
573
|
+
- [Installation](#-installation)
|
|
574
|
+
- [Quick Start](#-quick-start)
|
|
575
|
+
- [Features Overview](#-features-overview)
|
|
576
|
+
- [Requirements](#%EF%B8%8F-requirements)
|
|
577
|
+
- [Contributing](#-contributing)
|
|
578
|
+
|
|
490
579
|
## π€ Contributing
|
|
491
580
|
|
|
492
581
|
We welcome contributions! Here's how you can help:
|
|
@@ -532,6 +621,27 @@ bundle exec rake test # Coverage report in coverage/index.html
|
|
|
532
621
|
bundle exec rubocop
|
|
533
622
|
```
|
|
534
623
|
|
|
624
|
+
### π Test Coverage Notes
|
|
625
|
+
|
|
626
|
+
The test suite runs on **SQLite** for performance and portability. Current coverage: **91.45%** (1272 / 1391 lines).
|
|
627
|
+
|
|
628
|
+
**Database-Specific Features Not Covered:**
|
|
629
|
+
- **Predicable**: PostgreSQL array predicates (`_overlaps`, `_contains`, `_contained_by`) and JSONB predicates (`_has_key`, `_has_any_key`, `_has_all_keys`, `_jsonb_contains`) - lines 278-376 in `lib/better_model/predicable.rb`
|
|
630
|
+
- **Traceable**: PostgreSQL JSONB queries and MySQL JSON_EXTRACT queries for field-specific change tracking - lines 454-489 in `lib/better_model/traceable.rb`
|
|
631
|
+
- **Sortable**: MySQL NULLS emulation with CASE statements - lines 198-203 in `lib/better_model/sortable.rb`
|
|
632
|
+
|
|
633
|
+
These features are fully implemented with proper SQL sanitization but require manual testing on PostgreSQL/MySQL:
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
# Test on PostgreSQL
|
|
637
|
+
RAILS_ENV=test DATABASE_URL=postgresql://user:pass@localhost/better_model_test rails console
|
|
638
|
+
|
|
639
|
+
# Test on MySQL
|
|
640
|
+
RAILS_ENV=test DATABASE_URL=mysql2://user:pass@localhost/better_model_test rails console
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
All code has inline comments marking database-specific sections for maintainability.
|
|
644
|
+
|
|
535
645
|
### π Code Guidelines
|
|
536
646
|
|
|
537
647
|
- β¨ Follow the existing code style (enforced by RuboCop Omakase)
|
|
@@ -59,7 +59,7 @@ module BetterModel
|
|
|
59
59
|
next unless column
|
|
60
60
|
|
|
61
61
|
# Base predicates available for all column types
|
|
62
|
-
define_base_predicates(field_name)
|
|
62
|
+
define_base_predicates(field_name, column.type)
|
|
63
63
|
|
|
64
64
|
case column.type
|
|
65
65
|
when :string, :text
|
|
@@ -142,7 +142,7 @@ module BetterModel
|
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
# Genera predicati base: _eq, _not_eq, _present
|
|
145
|
-
def define_base_predicates(field_name)
|
|
145
|
+
def define_base_predicates(field_name, column_type = nil)
|
|
146
146
|
table = arel_table
|
|
147
147
|
field = table[field_name]
|
|
148
148
|
|
|
@@ -150,24 +150,26 @@ module BetterModel
|
|
|
150
150
|
scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
|
|
151
151
|
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
152
152
|
|
|
153
|
-
# Presence
|
|
154
|
-
|
|
153
|
+
# Presence - skip for string/text types as they get specialized version
|
|
154
|
+
# String types need to check for both nil and empty string
|
|
155
|
+
unless [ :string, :text ].include?(column_type)
|
|
156
|
+
scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
|
|
157
|
+
end
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
159
|
+
scopes_to_register = [ :"#{field_name}_eq", :"#{field_name}_not_eq" ]
|
|
160
|
+
scopes_to_register << :"#{field_name}_present" unless [ :string, :text ].include?(column_type)
|
|
161
|
+
|
|
162
|
+
register_predicable_scopes(*scopes_to_register)
|
|
161
163
|
end
|
|
162
164
|
|
|
163
165
|
# Genera predicati per campi stringa (12 scope)
|
|
164
166
|
# Base predicates (_eq, _not_eq) are defined separately
|
|
165
|
-
# _present is
|
|
167
|
+
# _present is defined here to handle both nil and empty strings
|
|
166
168
|
def define_string_predicates(field_name)
|
|
167
169
|
table = arel_table
|
|
168
170
|
field = table[field_name]
|
|
169
171
|
|
|
170
|
-
# String-specific presence check (
|
|
172
|
+
# String-specific presence check (checks both nil and empty string)
|
|
171
173
|
scope :"#{field_name}_present", -> { where(field.not_eq(nil).and(field.not_eq(""))) }
|
|
172
174
|
|
|
173
175
|
# Pattern matching (4)
|
|
@@ -273,6 +275,10 @@ module BetterModel
|
|
|
273
275
|
end
|
|
274
276
|
|
|
275
277
|
# Genera predicati per campi array PostgreSQL (3 scope)
|
|
278
|
+
#
|
|
279
|
+
# NOTA: Questo metodo non Γ¨ coperto da test automatici perchΓ© richiede
|
|
280
|
+
# PostgreSQL. I test vengono eseguiti su SQLite per performance.
|
|
281
|
+
# Testare manualmente su PostgreSQL con: rails console RAILS_ENV=test
|
|
276
282
|
def define_postgresql_array_predicates(field_name)
|
|
277
283
|
return unless postgresql_adapter?
|
|
278
284
|
|
|
@@ -325,6 +331,10 @@ module BetterModel
|
|
|
325
331
|
end
|
|
326
332
|
|
|
327
333
|
# Genera predicati per campi JSONB PostgreSQL (4 scope)
|
|
334
|
+
#
|
|
335
|
+
# NOTA: Questo metodo non Γ¨ coperto da test automatici perchΓ© richiede
|
|
336
|
+
# PostgreSQL con supporto JSONB. I test vengono eseguiti su SQLite per performance.
|
|
337
|
+
# Testare manualmente su PostgreSQL con: rails console RAILS_ENV=test
|
|
328
338
|
def define_postgresql_jsonb_predicates(field_name)
|
|
329
339
|
return unless postgresql_adapter?
|
|
330
340
|
|
|
@@ -188,6 +188,10 @@ module BetterModel
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
# Genera SQL per gestione NULL multi-database
|
|
191
|
+
#
|
|
192
|
+
# NOTA: Il blocco MySQL else qui sotto non Γ¨ coperto da test automatici
|
|
193
|
+
# perchΓ© i test vengono eseguiti su SQLite. Testare manualmente su MySQL
|
|
194
|
+
# con: rails console RAILS_ENV=test
|
|
191
195
|
def nulls_order_sql(field_name, direction, nulls_position)
|
|
192
196
|
quoted_field = connection.quote_column_name(field_name)
|
|
193
197
|
|
|
@@ -90,6 +90,9 @@ module BetterModel
|
|
|
90
90
|
module Stateable
|
|
91
91
|
extend ActiveSupport::Concern
|
|
92
92
|
|
|
93
|
+
# Thread-safe mutex for dynamic class creation
|
|
94
|
+
CLASS_CREATION_MUTEX = Mutex.new
|
|
95
|
+
|
|
93
96
|
included do
|
|
94
97
|
# Validazione ActiveRecord
|
|
95
98
|
unless ancestors.include?(ActiveRecord::Base)
|
|
@@ -188,6 +191,9 @@ module BetterModel
|
|
|
188
191
|
|
|
189
192
|
# Create or retrieve a StateTransition class for the given table name
|
|
190
193
|
#
|
|
194
|
+
# Thread-safe implementation using mutex to prevent race conditions
|
|
195
|
+
# when multiple threads try to create the same class simultaneously.
|
|
196
|
+
#
|
|
191
197
|
# @param table_name [String] Table name for state transitions
|
|
192
198
|
# @return [Class] StateTransition class
|
|
193
199
|
#
|
|
@@ -195,19 +201,27 @@ module BetterModel
|
|
|
195
201
|
# Create a unique class name based on table name
|
|
196
202
|
class_name = "#{table_name.camelize.singularize}"
|
|
197
203
|
|
|
198
|
-
#
|
|
204
|
+
# Fast path: check if class already exists (no lock needed)
|
|
199
205
|
if BetterModel.const_defined?(class_name, false)
|
|
200
206
|
return BetterModel.const_get(class_name)
|
|
201
207
|
end
|
|
202
208
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
# Slow path: acquire lock and create class
|
|
210
|
+
CLASS_CREATION_MUTEX.synchronize do
|
|
211
|
+
# Double-check after acquiring lock (another thread may have created it)
|
|
212
|
+
if BetterModel.const_defined?(class_name, false)
|
|
213
|
+
return BetterModel.const_get(class_name)
|
|
214
|
+
end
|
|
207
215
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
# Create new StateTransition class dynamically
|
|
217
|
+
transition_class = Class.new(BetterModel::StateTransition) do
|
|
218
|
+
self.table_name = table_name
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Register the class in BetterModel namespace
|
|
222
|
+
BetterModel.const_set(class_name, transition_class)
|
|
223
|
+
transition_class
|
|
224
|
+
end
|
|
211
225
|
end
|
|
212
226
|
|
|
213
227
|
# Setup dynamic methods per stati e transizioni
|
|
@@ -53,6 +53,9 @@ module BetterModel
|
|
|
53
53
|
module Traceable
|
|
54
54
|
extend ActiveSupport::Concern
|
|
55
55
|
|
|
56
|
+
# Thread-safe mutex for dynamic class creation
|
|
57
|
+
CLASS_CREATION_MUTEX = Mutex.new
|
|
58
|
+
|
|
56
59
|
included do
|
|
57
60
|
# Validazione ActiveRecord
|
|
58
61
|
unless ancestors.include?(ActiveRecord::Base)
|
|
@@ -175,25 +178,36 @@ module BetterModel
|
|
|
175
178
|
|
|
176
179
|
# Create or retrieve a Version class for the given table name
|
|
177
180
|
#
|
|
181
|
+
# Thread-safe implementation using mutex to prevent race conditions
|
|
182
|
+
# when multiple threads try to create the same class simultaneously.
|
|
183
|
+
#
|
|
178
184
|
# @param table_name [String] Table name for versions
|
|
179
185
|
# @return [Class] Version class
|
|
180
186
|
def create_version_class_for_table(table_name)
|
|
181
187
|
# Create a unique class name based on table name
|
|
182
188
|
class_name = "#{table_name.camelize.singularize}"
|
|
183
189
|
|
|
184
|
-
#
|
|
190
|
+
# Fast path: check if class already exists (no lock needed)
|
|
185
191
|
if BetterModel.const_defined?(class_name, false)
|
|
186
192
|
return BetterModel.const_get(class_name)
|
|
187
193
|
end
|
|
188
194
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
# Slow path: acquire lock and create class
|
|
196
|
+
CLASS_CREATION_MUTEX.synchronize do
|
|
197
|
+
# Double-check after acquiring lock (another thread may have created it)
|
|
198
|
+
if BetterModel.const_defined?(class_name, false)
|
|
199
|
+
return BetterModel.const_get(class_name)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Create new Version class dynamically
|
|
203
|
+
version_class = Class.new(BetterModel::Version) do
|
|
204
|
+
self.table_name = table_name
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Register the class in BetterModel namespace
|
|
208
|
+
BetterModel.const_set(class_name, version_class)
|
|
209
|
+
version_class
|
|
192
210
|
end
|
|
193
|
-
|
|
194
|
-
# Register the class in BetterModel namespace
|
|
195
|
-
BetterModel.const_set(class_name, version_class)
|
|
196
|
-
version_class
|
|
197
211
|
end
|
|
198
212
|
end
|
|
199
213
|
|
|
@@ -271,14 +285,10 @@ module BetterModel
|
|
|
271
285
|
def rollback_to(version, updated_by_id: nil, updated_reason: nil)
|
|
272
286
|
raise NotEnabledError unless self.class.traceable_enabled?
|
|
273
287
|
|
|
274
|
-
|
|
275
|
-
version = versions.find(version) if version.is_a?(Integer)
|
|
276
|
-
rescue ActiveRecord::RecordNotFound
|
|
277
|
-
raise ArgumentError, "Version not found"
|
|
278
|
-
end
|
|
288
|
+
version = versions.find(version) if version.is_a?(Integer)
|
|
279
289
|
|
|
280
|
-
raise
|
|
281
|
-
raise
|
|
290
|
+
raise ActiveRecord::RecordNotFound, "Version not found" unless version
|
|
291
|
+
raise ActiveRecord::RecordNotFound, "Version does not belong to this record" unless version.item == self
|
|
282
292
|
|
|
283
293
|
# Apply changes from version
|
|
284
294
|
if version.object_changes
|
|
@@ -352,7 +362,7 @@ module BetterModel
|
|
|
352
362
|
# Get final state for destroyed records
|
|
353
363
|
def tracked_final_state
|
|
354
364
|
self.class.traceable_fields.each_with_object({}) do |field, hash|
|
|
355
|
-
hash[field.to_s] = [send(field), nil]
|
|
365
|
+
hash[field.to_s] = [ send(field), nil ]
|
|
356
366
|
end
|
|
357
367
|
end
|
|
358
368
|
|
|
@@ -427,17 +437,59 @@ module BetterModel
|
|
|
427
437
|
|
|
428
438
|
private
|
|
429
439
|
|
|
440
|
+
# Check if database supports JSON/JSONB queries
|
|
441
|
+
def postgres?
|
|
442
|
+
ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def mysql?
|
|
446
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
|
447
|
+
adapter.include?("mysql") || adapter == "trilogy"
|
|
448
|
+
end
|
|
449
|
+
|
|
430
450
|
def execute_query
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
.where("#{@table_name}.object_changes->>'#{@field}' IS NOT NULL")
|
|
451
|
+
# Base query
|
|
452
|
+
query = @model_class.joins(:versions)
|
|
434
453
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
454
|
+
# NOTA: I blocchi PostgreSQL e MySQL qui sotto non sono coperti da test
|
|
455
|
+
# automatici perchΓ© i test vengono eseguiti su SQLite per performance.
|
|
456
|
+
# Testare manualmente su PostgreSQL/MySQL con: rails console RAILS_ENV=test
|
|
457
|
+
|
|
458
|
+
# PostgreSQL: Use JSONB operators for better performance
|
|
459
|
+
if postgres?
|
|
460
|
+
query = query.where("#{@table_name}.object_changes ? :field", field: @field)
|
|
461
|
+
|
|
462
|
+
if @from_value
|
|
463
|
+
query = query.where("#{@table_name}.object_changes->>'#{@field}' LIKE ?", "%#{@from_value}%")
|
|
464
|
+
end
|
|
438
465
|
|
|
439
|
-
|
|
440
|
-
|
|
466
|
+
if @to_value
|
|
467
|
+
query = query.where("#{@table_name}.object_changes->>'#{@field}' LIKE ?", "%#{@to_value}%")
|
|
468
|
+
end
|
|
469
|
+
# MySQL 5.7+: Use JSON_EXTRACT
|
|
470
|
+
elsif mysql?
|
|
471
|
+
query = query.where("JSON_EXTRACT(#{@table_name}.object_changes, '$.#{@field}') IS NOT NULL")
|
|
472
|
+
|
|
473
|
+
if @from_value
|
|
474
|
+
query = query.where("JSON_EXTRACT(#{@table_name}.object_changes, '$.#{@field}') LIKE ?", "%#{@from_value}%")
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
if @to_value
|
|
478
|
+
query = query.where("JSON_EXTRACT(#{@table_name}.object_changes, '$.#{@field}') LIKE ?", "%#{@to_value}%")
|
|
479
|
+
end
|
|
480
|
+
# SQLite or fallback: Use text-based search (limited functionality)
|
|
481
|
+
else
|
|
482
|
+
Rails.logger.warn "Traceable field-specific queries may have limited functionality on #{ActiveRecord::Base.connection.adapter_name}"
|
|
483
|
+
|
|
484
|
+
query = query.where("#{@table_name}.object_changes LIKE ?", "%\"#{@field}\"%")
|
|
485
|
+
|
|
486
|
+
if @from_value
|
|
487
|
+
query = query.where("#{@table_name}.object_changes LIKE ?", "%#{@from_value}%")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
if @to_value
|
|
491
|
+
query = query.where("#{@table_name}.object_changes LIKE ?", "%#{@to_value}%")
|
|
492
|
+
end
|
|
441
493
|
end
|
|
442
494
|
|
|
443
495
|
query.distinct
|
|
@@ -135,11 +135,11 @@ module BetterModel
|
|
|
135
135
|
validate do
|
|
136
136
|
condition_met = if condition.is_a?(Symbol)
|
|
137
137
|
send(condition)
|
|
138
|
-
|
|
138
|
+
elsif condition.is_a?(Proc)
|
|
139
139
|
instance_exec(&condition)
|
|
140
|
-
|
|
140
|
+
else
|
|
141
141
|
raise ArgumentError, "Condition must be a Symbol or Proc"
|
|
142
|
-
|
|
142
|
+
end
|
|
143
143
|
|
|
144
144
|
condition_met = !condition_met if negate
|
|
145
145
|
|
data/lib/better_model/version.rb
CHANGED