better_model 1.0.0 โ 1.1.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 +317 -85
- data/lib/better_model/archivable.rb +10 -10
- data/lib/better_model/predicable.rb +21 -44
- data/lib/better_model/state_transition.rb +106 -0
- data/lib/better_model/stateable/configurator.rb +300 -0
- data/lib/better_model/stateable/errors.rb +45 -0
- data/lib/better_model/stateable/guard.rb +87 -0
- data/lib/better_model/stateable/transition.rb +143 -0
- data/lib/better_model/stateable.rb +340 -0
- data/lib/better_model/traceable.rb +446 -0
- data/lib/better_model/validatable/business_rule_validator.rb +47 -0
- data/lib/better_model/validatable/configurator.rb +245 -0
- data/lib/better_model/validatable/order_validator.rb +77 -0
- data/lib/better_model/validatable.rb +270 -0
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model/version_record.rb +63 -0
- data/lib/better_model.rb +15 -2
- data/lib/generators/better_model/stateable/install_generator.rb +37 -0
- data/lib/generators/better_model/stateable/stateable_generator.rb +50 -0
- data/lib/generators/better_model/stateable/templates/README +38 -0
- data/lib/generators/better_model/stateable/templates/install_migration.rb.tt +20 -0
- data/lib/generators/better_model/stateable/templates/migration.rb.tt +9 -0
- data/lib/generators/better_model/traceable/templates/create_table_migration.rb.tt +28 -0
- data/lib/generators/better_model/traceable/traceable_generator.rb +77 -0
- metadata +22 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75b360674e8e3baf46d99e536d2e012f0bbeea56169f2133d65193f9e093973b
|
|
4
|
+
data.tar.gz: e4feb8e169012f8b94fe9f9ad720838d68d24df3e811c4de2fb1f018e4edef9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc082d6e0b93b1750f73ab4878f26f7f99aa2080aeb05c8382f5e4b47ee6287402b984686cebe665a7ffa1b154683aa00ddb8b7a78e841d41f3446a27d104957
|
|
7
|
+
data.tar.gz: 5fd80c99b6e83ad69870226c06c949ba11b8e15e44883ba5b33c7e2ca8e79e0f0c79f8465ad6c176ba6726f8b3947819b39feaa94a316935fc2045f41b78a13c
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# BetterModel
|
|
1
|
+
# BetterModel ๐
|
|
2
2
|
|
|
3
|
-
BetterModel is a Rails engine gem (Rails 8.1+) that provides powerful extensions for ActiveRecord models, including declarative status management, permissions, archiving, sorting, filtering, and unified search capabilities.
|
|
3
|
+
BetterModel is a Rails engine gem (Rails 8.1+) that provides powerful extensions for ActiveRecord models, including declarative status management, permissions, state machines, validations, archiving, change tracking, sorting, filtering, and unified search capabilities.
|
|
4
4
|
|
|
5
|
-
## Installation
|
|
5
|
+
## ๐ฆ Installation
|
|
6
6
|
|
|
7
7
|
Add this line to your application's Gemfile:
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ Or install it yourself as:
|
|
|
20
20
|
$ gem install better_model
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
## Quick Start
|
|
23
|
+
## โก Quick Start
|
|
24
24
|
|
|
25
25
|
Simply include `BetterModel` in your model to get all features:
|
|
26
26
|
|
|
@@ -52,7 +52,46 @@ class Article < ApplicationRecord
|
|
|
52
52
|
skip_archived_by_default true # Hide archived records by default
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
# 6.
|
|
55
|
+
# 6. VALIDATABLE - Declarative validation system (opt-in)
|
|
56
|
+
validatable do
|
|
57
|
+
# Basic validations
|
|
58
|
+
validate :title, :content, presence: true
|
|
59
|
+
|
|
60
|
+
# Conditional validations
|
|
61
|
+
validate_if :is_published? do
|
|
62
|
+
validate :published_at, presence: true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Cross-field validations
|
|
66
|
+
validate_order :starts_at, :before, :ends_at
|
|
67
|
+
|
|
68
|
+
# Business rules
|
|
69
|
+
validate_business_rule :valid_category
|
|
70
|
+
|
|
71
|
+
# Validation groups (multi-step forms)
|
|
72
|
+
validation_group :step1, [:title, :content]
|
|
73
|
+
validation_group :step2, [:published_at]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 7. STATEABLE - Declarative state machine (opt-in)
|
|
77
|
+
stateable do
|
|
78
|
+
# Define states
|
|
79
|
+
state :draft, initial: true
|
|
80
|
+
state :published
|
|
81
|
+
state :archived
|
|
82
|
+
|
|
83
|
+
# Define transitions with guards and callbacks
|
|
84
|
+
transition :publish, from: :draft, to: :published do
|
|
85
|
+
guard { valid? }
|
|
86
|
+
guard if: :is_ready_for_publishing? # Statusable integration
|
|
87
|
+
before { set_published_at }
|
|
88
|
+
after { notify_subscribers }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
transition :archive, from: [:draft, :published], to: :archived
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 8. SEARCHABLE - Configure unified search interface
|
|
56
95
|
searchable do
|
|
57
96
|
per_page 25
|
|
58
97
|
max_per_page 100
|
|
@@ -62,41 +101,55 @@ class Article < ApplicationRecord
|
|
|
62
101
|
end
|
|
63
102
|
```
|
|
64
103
|
|
|
65
|
-
Now you can use all the features
|
|
104
|
+
๐ก **Now you can use all the features:**
|
|
66
105
|
|
|
67
106
|
```ruby
|
|
68
|
-
# Check statuses
|
|
107
|
+
# โ
Check statuses
|
|
69
108
|
article.is?(:draft) # => true/false
|
|
70
109
|
article.is_published? # => true/false
|
|
71
110
|
article.statuses # => { draft: true, published: false, ... }
|
|
72
111
|
|
|
73
|
-
# Check permissions
|
|
112
|
+
# ๐ Check permissions
|
|
74
113
|
article.permit?(:edit) # => true/false
|
|
75
114
|
article.permit_delete? # => true/false
|
|
76
115
|
article.permissions # => { edit: true, delete: true, ... }
|
|
77
116
|
|
|
78
|
-
# Sort
|
|
117
|
+
# โฌ๏ธ Sort
|
|
79
118
|
Article.sort_title_asc
|
|
80
119
|
Article.sort_view_count_desc
|
|
81
120
|
Article.sort_published_at_desc
|
|
82
121
|
|
|
83
|
-
# Filter with predicates
|
|
122
|
+
# ๐ Filter with predicates
|
|
84
123
|
Article.status_eq("published")
|
|
85
124
|
Article.title_cont("Rails")
|
|
86
125
|
Article.view_count_gteq(100)
|
|
87
126
|
Article.published_at_present
|
|
88
127
|
|
|
89
|
-
# Archive records
|
|
128
|
+
# ๐๏ธ Archive records
|
|
90
129
|
article.archive!(by: current_user, reason: "Outdated")
|
|
91
130
|
article.archived? # => true
|
|
92
131
|
article.restore!
|
|
93
132
|
|
|
94
|
-
# Query archived records
|
|
133
|
+
# ๐ Query archived records
|
|
95
134
|
Article.archived
|
|
96
135
|
Article.not_archived
|
|
97
136
|
Article.archived_recently(7.days)
|
|
98
137
|
|
|
99
|
-
#
|
|
138
|
+
# โ
Validate with groups (multi-step forms)
|
|
139
|
+
article.valid?(:step1) # Validate only step1 fields
|
|
140
|
+
article.valid?(:step2) # Validate only step2 fields
|
|
141
|
+
article.errors_for_group(:step1) # Get errors for step1 only
|
|
142
|
+
|
|
143
|
+
# ๐ State machine transitions
|
|
144
|
+
article.state # => "draft"
|
|
145
|
+
article.draft? # => true
|
|
146
|
+
article.can_publish? # => true (checks guards)
|
|
147
|
+
article.publish! # Executes transition with guards & callbacks
|
|
148
|
+
article.published? # => true
|
|
149
|
+
article.state_transitions # History of all transitions
|
|
150
|
+
article.transition_history # Formatted history array
|
|
151
|
+
|
|
152
|
+
# ๐ Unified search with filters, sorting, and pagination
|
|
100
153
|
Article.search(
|
|
101
154
|
{ status_eq: "published", view_count_gteq: 50 },
|
|
102
155
|
orders: [:sort_published_at_desc],
|
|
@@ -104,7 +157,7 @@ Article.search(
|
|
|
104
157
|
)
|
|
105
158
|
```
|
|
106
159
|
|
|
107
|
-
### Including Individual Concerns (Advanced)
|
|
160
|
+
### ๐ฏ Including Individual Concerns (Advanced)
|
|
108
161
|
|
|
109
162
|
If you only need specific features, you can include individual concerns:
|
|
110
163
|
|
|
@@ -115,19 +168,21 @@ class Article < ApplicationRecord
|
|
|
115
168
|
include BetterModel::Archivable # Only archiving
|
|
116
169
|
include BetterModel::Sortable # Only sorting
|
|
117
170
|
include BetterModel::Predicable # Only filtering
|
|
171
|
+
include BetterModel::Validatable # Only validations
|
|
172
|
+
include BetterModel::Stateable # Only state machine
|
|
118
173
|
include BetterModel::Searchable # Only search (requires Predicable & Sortable)
|
|
119
174
|
|
|
120
175
|
# Define your features...
|
|
121
176
|
end
|
|
122
177
|
```
|
|
123
178
|
|
|
124
|
-
## Requirements
|
|
179
|
+
## โ๏ธ Requirements
|
|
125
180
|
|
|
126
181
|
- **Ruby:** 3.0 or higher
|
|
127
182
|
- **Rails:** 8.1 or higher
|
|
128
183
|
- **ActiveRecord:** Included with Rails
|
|
129
184
|
|
|
130
|
-
## Database Compatibility
|
|
185
|
+
## ๐พ Database Compatibility
|
|
131
186
|
|
|
132
187
|
BetterModel works with all databases supported by ActiveRecord:
|
|
133
188
|
|
|
@@ -143,20 +198,20 @@ BetterModel works with all databases supported by ActiveRecord:
|
|
|
143
198
|
- Array predicates: `overlaps`, `contains`, `contained_by`
|
|
144
199
|
- JSONB predicates: `has_key`, `has_any_key`, `has_all_keys`, `jsonb_contains`
|
|
145
200
|
|
|
146
|
-
## Features
|
|
201
|
+
## ๐ Features
|
|
147
202
|
|
|
148
|
-
BetterModel provides
|
|
203
|
+
BetterModel provides eight powerful concerns that work together seamlessly:
|
|
149
204
|
|
|
150
205
|
### ๐ Statusable - Declarative Status Management
|
|
151
206
|
|
|
152
207
|
Define derived statuses dynamically based on model attributes - no database columns needed!
|
|
153
208
|
|
|
154
|
-
|
|
155
|
-
- Declarative DSL with clear, readable conditions
|
|
156
|
-
- Statuses calculated in real-time from model attributes
|
|
157
|
-
- Reference other statuses in conditions
|
|
158
|
-
- Automatic method generation (`is_draft?`, `is_published?`)
|
|
159
|
-
- Thread-safe with immutable registry
|
|
209
|
+
**๐ฏ Key Benefits:**
|
|
210
|
+
- โจ Declarative DSL with clear, readable conditions
|
|
211
|
+
- โก Statuses calculated in real-time from model attributes
|
|
212
|
+
- ๐ Reference other statuses in conditions
|
|
213
|
+
- ๐ค Automatic method generation (`is_draft?`, `is_published?`)
|
|
214
|
+
- ๐ Thread-safe with immutable registry
|
|
160
215
|
|
|
161
216
|
**[๐ Full Documentation โ](docs/statusable.md)**
|
|
162
217
|
|
|
@@ -166,12 +221,12 @@ Define derived statuses dynamically based on model attributes - no database colu
|
|
|
166
221
|
|
|
167
222
|
Define permissions dynamically based on model state and statuses - perfect for authorization logic!
|
|
168
223
|
|
|
169
|
-
|
|
170
|
-
- Declarative DSL following Statusable pattern
|
|
171
|
-
- Permissions calculated from model state
|
|
172
|
-
- Reference statuses in permission logic
|
|
173
|
-
- Automatic method generation (`permit_edit?`, `permit_delete?`)
|
|
174
|
-
- Thread-safe with immutable registry
|
|
224
|
+
**๐ฏ Key Benefits:**
|
|
225
|
+
- โจ Declarative DSL following Statusable pattern
|
|
226
|
+
- โก Permissions calculated from model state
|
|
227
|
+
- ๐ Reference statuses in permission logic
|
|
228
|
+
- ๐ค Automatic method generation (`permit_edit?`, `permit_delete?`)
|
|
229
|
+
- ๐ Thread-safe with immutable registry
|
|
175
230
|
|
|
176
231
|
**[๐ Full Documentation โ](docs/permissible.md)**
|
|
177
232
|
|
|
@@ -181,30 +236,66 @@ Define permissions dynamically based on model state and statuses - perfect for a
|
|
|
181
236
|
|
|
182
237
|
Soft-delete records with archive tracking, audit trails, and restoration capabilities.
|
|
183
238
|
|
|
184
|
-
|
|
185
|
-
- Opt-in activation: only enabled when explicitly configured
|
|
186
|
-
- Archive and restore methods with optional tracking
|
|
187
|
-
- Status methods: `archived?` and `active?`
|
|
188
|
-
- Semantic scopes: `archived`, `not_archived`, `archived_only`
|
|
189
|
-
- Helper predicates: `archived_today`, `archived_this_week`, `archived_recently`
|
|
190
|
-
- Optional default scope to hide archived records
|
|
191
|
-
- Migration generator with flexible options
|
|
192
|
-
- Thread-safe with immutable configuration
|
|
239
|
+
**๐ฏ Key Benefits:**
|
|
240
|
+
- ๐๏ธ Opt-in activation: only enabled when explicitly configured
|
|
241
|
+
- ๐ Archive and restore methods with optional tracking
|
|
242
|
+
- โ
Status methods: `archived?` and `active?`
|
|
243
|
+
- ๐ Semantic scopes: `archived`, `not_archived`, `archived_only`
|
|
244
|
+
- ๐ ๏ธ Helper predicates: `archived_today`, `archived_this_week`, `archived_recently`
|
|
245
|
+
- ๐ป Optional default scope to hide archived records
|
|
246
|
+
- ๐ Migration generator with flexible options
|
|
247
|
+
- ๐ Thread-safe with immutable configuration
|
|
193
248
|
|
|
194
249
|
**[๐ Full Documentation โ](docs/archivable.md)**
|
|
195
250
|
|
|
196
251
|
---
|
|
197
252
|
|
|
253
|
+
### โ
Validatable - Declarative Validation System
|
|
254
|
+
|
|
255
|
+
Define validations declaratively with support for conditional rules, cross-field validation, business rules, and validation groups.
|
|
256
|
+
|
|
257
|
+
**๐ฏ Key Benefits:**
|
|
258
|
+
- ๐๏ธ Opt-in activation: only enabled when explicitly configured
|
|
259
|
+
- โจ Declarative DSL for all validation types
|
|
260
|
+
- ๐ Conditional validations: `validate_if` / `validate_unless`
|
|
261
|
+
- ๐ Cross-field validations: `validate_order` for date/number comparisons
|
|
262
|
+
- ๐ผ Business rules: delegate complex logic to custom methods
|
|
263
|
+
- ๐ Validation groups: partial validation for multi-step forms
|
|
264
|
+
- ๐ Thread-safe with immutable configuration
|
|
265
|
+
|
|
266
|
+
**[๐ Full Documentation โ](docs/validatable.md)**
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### ๐ Stateable - Declarative State Machine
|
|
271
|
+
|
|
272
|
+
Define state machines declaratively with transitions, guards, validations, and callbacks for robust workflow management.
|
|
273
|
+
|
|
274
|
+
**๐ฏ Key Benefits:**
|
|
275
|
+
- ๐๏ธ Opt-in activation: only enabled when explicitly configured
|
|
276
|
+
- โจ Declarative DSL for states and transitions
|
|
277
|
+
- ๐ก๏ธ Guards: preconditions with lambda, methods, or Statusable predicates
|
|
278
|
+
- โ
Validations: custom validation logic per transition
|
|
279
|
+
- ๐ Callbacks: before/after/around hooks for each transition
|
|
280
|
+
- ๐ State history tracking with customizable table names
|
|
281
|
+
- ๐ค Dynamic methods: `pending?`, `confirm!`, `can_confirm?`
|
|
282
|
+
- ๐ Integration with Statusable for complex guard logic
|
|
283
|
+
- ๐ Thread-safe with immutable configuration
|
|
284
|
+
|
|
285
|
+
**[๐ Full Documentation โ](docs/stateable.md)**
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
198
289
|
### โฌ๏ธ Sortable - Type-Aware Sorting Scopes
|
|
199
290
|
|
|
200
291
|
Generate intelligent sorting scopes automatically with database-specific optimizations and NULL handling.
|
|
201
292
|
|
|
202
|
-
|
|
203
|
-
- Type-aware scope generation (string, numeric, datetime, boolean)
|
|
204
|
-
- Case-insensitive sorting for strings
|
|
205
|
-
- Database-specific NULLS FIRST/LAST support
|
|
206
|
-
- Sort by multiple fields with chaining
|
|
207
|
-
- Optimized queries with proper indexing support
|
|
293
|
+
**๐ฏ Key Benefits:**
|
|
294
|
+
- ๐ฏ Type-aware scope generation (string, numeric, datetime, boolean)
|
|
295
|
+
- ๐ค Case-insensitive sorting for strings
|
|
296
|
+
- ๐พ Database-specific NULLS FIRST/LAST support
|
|
297
|
+
- ๐ Sort by multiple fields with chaining
|
|
298
|
+
- โก Optimized queries with proper indexing support
|
|
208
299
|
|
|
209
300
|
**[๐ Full Documentation โ](docs/sortable.md)**
|
|
210
301
|
|
|
@@ -214,13 +305,13 @@ Generate intelligent sorting scopes automatically with database-specific optimiz
|
|
|
214
305
|
|
|
215
306
|
Generate comprehensive predicate scopes for filtering and searching with support for all data types.
|
|
216
307
|
|
|
217
|
-
|
|
218
|
-
- Complete coverage: string, numeric, datetime, boolean, null predicates
|
|
219
|
-
- Type-safe predicates based on column type
|
|
220
|
-
- Case-insensitive string matching
|
|
221
|
-
- Range queries (between) for numerics and dates
|
|
222
|
-
- PostgreSQL array and JSONB support
|
|
223
|
-
- Chainable with standard ActiveRecord queries
|
|
308
|
+
**๐ฏ Key Benefits:**
|
|
309
|
+
- โ
Complete coverage: string, numeric, datetime, boolean, null predicates
|
|
310
|
+
- ๐ Type-safe predicates based on column type
|
|
311
|
+
- ๐ค Case-insensitive string matching
|
|
312
|
+
- ๐ Range queries (between) for numerics and dates
|
|
313
|
+
- ๐ PostgreSQL array and JSONB support
|
|
314
|
+
- ๐ Chainable with standard ActiveRecord queries
|
|
224
315
|
|
|
225
316
|
**[๐ Full Documentation โ](docs/predicable.md)**
|
|
226
317
|
|
|
@@ -230,57 +321,198 @@ Generate comprehensive predicate scopes for filtering and searching with support
|
|
|
230
321
|
|
|
231
322
|
Orchestrate Predicable and Sortable into a powerful, secure search interface with pagination and security.
|
|
232
323
|
|
|
233
|
-
|
|
234
|
-
- Unified API: single `search()` method for all operations
|
|
235
|
-
- OR conditions for complex logic
|
|
236
|
-
- Built-in pagination with DoS protection (max_per_page)
|
|
237
|
-
- Security enforcement with required predicates
|
|
238
|
-
- Default ordering configuration
|
|
239
|
-
- Strong parameters integration
|
|
240
|
-
- Type-safe validation of all parameters
|
|
324
|
+
**๐ฏ Key Benefits:**
|
|
325
|
+
- ๐ฏ Unified API: single `search()` method for all operations
|
|
326
|
+
- ๐ OR conditions for complex logic
|
|
327
|
+
- ๐ Built-in pagination with DoS protection (max_per_page)
|
|
328
|
+
- ๐ Security enforcement with required predicates
|
|
329
|
+
- โ๏ธ Default ordering configuration
|
|
330
|
+
- ๐ช Strong parameters integration
|
|
331
|
+
- โ
Type-safe validation of all parameters
|
|
241
332
|
|
|
242
333
|
**[๐ Full Documentation โ](docs/searchable.md)**
|
|
243
334
|
|
|
244
335
|
---
|
|
245
336
|
|
|
246
|
-
|
|
337
|
+
### ๐ Traceable - Audit Trail & Change Tracking
|
|
338
|
+
|
|
339
|
+
Track all changes to your records with comprehensive audit trail functionality, time-travel queries, and rollback capabilities.
|
|
340
|
+
|
|
341
|
+
**๐ฏ Key Benefits:**
|
|
342
|
+
- ๐๏ธ Opt-in activation: only enabled when explicitly configured
|
|
343
|
+
- ๐ค Automatic change tracking on create/update/destroy
|
|
344
|
+
- โฐ Time-travel: reconstruct record state at any point in time
|
|
345
|
+
- โฉ๏ธ Rollback: restore to previous versions
|
|
346
|
+
- ๐ Audit trail with who/why tracking
|
|
347
|
+
- ๐ Query changes by user, date range, or field transitions
|
|
348
|
+
- ๐๏ธ Flexible table naming: per-model tables (default), shared table, or custom names
|
|
349
|
+
|
|
350
|
+
**[๐ Full Documentation โ](docs/traceable.md)**
|
|
351
|
+
|
|
352
|
+
#### ๐ Quick Setup
|
|
353
|
+
|
|
354
|
+
**1๏ธโฃ Step 1: Create the versions table**
|
|
355
|
+
|
|
356
|
+
By default, each model gets its own versions table (`{model}_versions`):
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# Creates migration for article_versions table
|
|
360
|
+
rails g better_model:traceable Article --create-table
|
|
361
|
+
rails db:migrate
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Or use a custom table name:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Creates migration for custom table name
|
|
368
|
+
rails g better_model:traceable Article --create-table --table-name=audit_log
|
|
369
|
+
rails db:migrate
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**2๏ธโฃ Step 2: Enable in your model**
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
class Article < ApplicationRecord
|
|
376
|
+
include BetterModel
|
|
377
|
+
|
|
378
|
+
# Activate traceable (opt-in)
|
|
379
|
+
traceable do
|
|
380
|
+
track :status, :title, :published_at # Fields to track
|
|
381
|
+
# versions_table 'audit_log' # Optional: custom table (default: article_versions)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**๐ก Usage:**
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
# ๐ค Automatic tracking on changes
|
|
390
|
+
article.update!(status: "published", updated_by_id: user.id, updated_reason: "Approved")
|
|
391
|
+
|
|
392
|
+
# ๐ Query version history
|
|
393
|
+
article.versions # All versions (ordered desc)
|
|
394
|
+
article.changes_for(:status) # Changes for specific field
|
|
395
|
+
article.audit_trail # Full formatted history
|
|
396
|
+
|
|
397
|
+
# โฐ Time-travel: reconstruct state at specific time
|
|
398
|
+
past_article = article.as_of(3.days.ago)
|
|
399
|
+
past_article.status # => "draft" (what it was 3 days ago)
|
|
400
|
+
|
|
401
|
+
# โฉ๏ธ Rollback to previous version
|
|
402
|
+
version = article.versions.where(event: "updated").first
|
|
403
|
+
article.rollback_to(version, updated_by_id: user.id, updated_reason: "Mistake")
|
|
404
|
+
|
|
405
|
+
# ๐ Class-level queries
|
|
406
|
+
Article.changed_by(user.id) # Records changed by user
|
|
407
|
+
Article.changed_between(1.week.ago, Time.current) # Changes in period
|
|
408
|
+
Article.status_changed_from("draft").to("published") # Specific transitions (PostgreSQL)
|
|
409
|
+
|
|
410
|
+
# ๐ฆ Integration with as_json
|
|
411
|
+
article.as_json(include_audit_trail: true) # Include full history in JSON
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**๐พ Database Schema:**
|
|
415
|
+
|
|
416
|
+
By default, each model gets its own versions table (e.g., `article_versions` for Article model).
|
|
417
|
+
You can also use a shared table across multiple models or a custom table name.
|
|
418
|
+
|
|
419
|
+
| Column | Type | Description |
|
|
420
|
+
|--------|------|-------------|
|
|
421
|
+
| `item_type` | string | Polymorphic model name |
|
|
422
|
+
| `item_id` | integer | Polymorphic record ID |
|
|
423
|
+
| `event` | string | Event type: created/updated/destroyed |
|
|
424
|
+
| `object_changes` | json | Before/after values for tracked fields |
|
|
425
|
+
| `updated_by_id` | integer | Optional: user who made the change |
|
|
426
|
+
| `updated_reason` | string | Optional: reason for the change |
|
|
427
|
+
| `created_at` | datetime | When the change occurred |
|
|
428
|
+
|
|
429
|
+
**๐๏ธ Table Naming Options:**
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
# 1๏ธโฃ Option 1: Per-model table (default)
|
|
433
|
+
class Article < ApplicationRecord
|
|
434
|
+
traceable do
|
|
435
|
+
track :status
|
|
436
|
+
# Uses article_versions table automatically
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# 2๏ธโฃ Option 2: Custom table name
|
|
441
|
+
class Article < ApplicationRecord
|
|
442
|
+
traceable do
|
|
443
|
+
track :status
|
|
444
|
+
versions_table 'audit_log' # Uses audit_log table
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# 3๏ธโฃ Option 3: Shared table across models
|
|
449
|
+
class Article < ApplicationRecord
|
|
450
|
+
traceable do
|
|
451
|
+
track :status
|
|
452
|
+
versions_table 'versions' # Shared table
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
class User < ApplicationRecord
|
|
457
|
+
traceable do
|
|
458
|
+
track :email
|
|
459
|
+
versions_table 'versions' # Same shared table
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**๐ Optional Tracking:**
|
|
465
|
+
|
|
466
|
+
To track who made changes and why, simply set attributes before saving:
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
article.updated_by_id = current_user.id
|
|
470
|
+
article.updated_reason = "Fixed typo"
|
|
471
|
+
article.update!(title: "Corrected Title")
|
|
472
|
+
|
|
473
|
+
# The version will automatically include updated_by_id and updated_reason
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## ๐ Version & Changelog
|
|
247
479
|
|
|
248
480
|
**Current Version:** 1.0.0
|
|
249
481
|
|
|
250
482
|
See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
|
|
251
483
|
|
|
252
|
-
## Support & Community
|
|
484
|
+
## ๐ฌ Support & Community
|
|
253
485
|
|
|
254
|
-
- **Issues & Bugs:** [GitHub Issues](https://github.com/alessiobussolari/better_model/issues)
|
|
255
|
-
- **Source Code:** [GitHub Repository](https://github.com/alessiobussolari/better_model)
|
|
256
|
-
- **Documentation:** This README and detailed docs in `docs/` directory
|
|
486
|
+
- ๐ **Issues & Bugs:** [GitHub Issues](https://github.com/alessiobussolari/better_model/issues)
|
|
487
|
+
- ๐ป **Source Code:** [GitHub Repository](https://github.com/alessiobussolari/better_model)
|
|
488
|
+
- ๐ **Documentation:** This README and detailed docs in `docs/` directory
|
|
257
489
|
|
|
258
|
-
## Contributing
|
|
490
|
+
## ๐ค Contributing
|
|
259
491
|
|
|
260
492
|
We welcome contributions! Here's how you can help:
|
|
261
493
|
|
|
262
|
-
### Reporting Bugs
|
|
494
|
+
### ๐ Reporting Bugs
|
|
263
495
|
|
|
264
|
-
1. Check if the issue already exists in [GitHub Issues](https://github.com/alessiobussolari/better_model/issues)
|
|
265
|
-
2. Create a new issue with:
|
|
496
|
+
1. โ
Check if the issue already exists in [GitHub Issues](https://github.com/alessiobussolari/better_model/issues)
|
|
497
|
+
2. ๐ Create a new issue with:
|
|
266
498
|
- Clear description of the problem
|
|
267
499
|
- Steps to reproduce
|
|
268
500
|
- Expected vs actual behavior
|
|
269
501
|
- Ruby/Rails versions
|
|
270
502
|
- Database adapter
|
|
271
503
|
|
|
272
|
-
### Submitting Pull Requests
|
|
504
|
+
### ๐ Submitting Pull Requests
|
|
273
505
|
|
|
274
|
-
1. Fork the repository
|
|
275
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
276
|
-
3. Make your changes with tests
|
|
277
|
-
4. Run the test suite (`bundle exec rake test`)
|
|
278
|
-
5. Ensure RuboCop passes (`bundle exec rubocop`)
|
|
279
|
-
6. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
280
|
-
7. Push to the branch (`git push origin feature/amazing-feature`)
|
|
281
|
-
8. Open a Pull Request
|
|
506
|
+
1. ๐ด Fork the repository
|
|
507
|
+
2. ๐ฟ Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
508
|
+
3. โ๏ธ Make your changes with tests
|
|
509
|
+
4. ๐งช Run the test suite (`bundle exec rake test`)
|
|
510
|
+
5. ๐
Ensure RuboCop passes (`bundle exec rubocop`)
|
|
511
|
+
6. ๐พ Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
512
|
+
7. ๐ค Push to the branch (`git push origin feature/amazing-feature`)
|
|
513
|
+
8. ๐ Open a Pull Request
|
|
282
514
|
|
|
283
|
-
### Development Setup
|
|
515
|
+
### ๐ง Development Setup
|
|
284
516
|
|
|
285
517
|
```bash
|
|
286
518
|
# Clone your fork
|
|
@@ -300,13 +532,13 @@ bundle exec rake test # Coverage report in coverage/index.html
|
|
|
300
532
|
bundle exec rubocop
|
|
301
533
|
```
|
|
302
534
|
|
|
303
|
-
### Code Guidelines
|
|
535
|
+
### ๐ Code Guidelines
|
|
304
536
|
|
|
305
|
-
- Follow the existing code style (enforced by RuboCop Omakase)
|
|
306
|
-
- Write tests for new features
|
|
307
|
-
- Update documentation (README) for user-facing changes
|
|
308
|
-
- Keep pull requests focused (one feature/fix per PR)
|
|
537
|
+
- โจ Follow the existing code style (enforced by RuboCop Omakase)
|
|
538
|
+
- ๐งช Write tests for new features
|
|
539
|
+
- ๐ Update documentation (README) for user-facing changes
|
|
540
|
+
- ๐ฏ Keep pull requests focused (one feature/fix per PR)
|
|
309
541
|
|
|
310
|
-
## License
|
|
542
|
+
## ๐ License
|
|
311
543
|
|
|
312
544
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -115,7 +115,7 @@ module BetterModel
|
|
|
115
115
|
|
|
116
116
|
# Applica default scope SOLO se configurato
|
|
117
117
|
if archivable_config[:skip_archived_by_default]
|
|
118
|
-
default_scope -> {
|
|
118
|
+
default_scope -> { where(archived_at: nil) }
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
end
|
|
@@ -124,19 +124,19 @@ module BetterModel
|
|
|
124
124
|
#
|
|
125
125
|
# @return [ActiveRecord::Relation]
|
|
126
126
|
def archived_only
|
|
127
|
-
raise
|
|
127
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
128
128
|
unscoped.archived
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
# Helper: alias per archived_at_today
|
|
132
132
|
def archived_today
|
|
133
|
-
raise
|
|
133
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
134
134
|
archived_at_today
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# Helper: alias per archived_at_this_week
|
|
138
138
|
def archived_this_week
|
|
139
|
-
raise
|
|
139
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
140
140
|
archived_at_this_week
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -145,7 +145,7 @@ module BetterModel
|
|
|
145
145
|
# @param duration [ActiveSupport::Duration] Durata (es: 7.days)
|
|
146
146
|
# @return [ActiveRecord::Relation]
|
|
147
147
|
def archived_recently(duration = 7.days)
|
|
148
|
-
raise
|
|
148
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
149
149
|
archived_at_within(duration)
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -164,10 +164,10 @@ module BetterModel
|
|
|
164
164
|
# @param by [Integer, Object] ID utente o oggetto user (opzionale)
|
|
165
165
|
# @param reason [String] Motivo dell'archiviazione (opzionale)
|
|
166
166
|
# @return [self]
|
|
167
|
-
# @raise [
|
|
167
|
+
# @raise [ArchivableNotEnabledError] se archivable non รจ attivo
|
|
168
168
|
# @raise [AlreadyArchivedError] se giร archiviato
|
|
169
169
|
def archive!(by: nil, reason: nil)
|
|
170
|
-
raise
|
|
170
|
+
raise ArchivableNotEnabledError unless self.class.archivable_enabled?
|
|
171
171
|
raise AlreadyArchivedError, "Record is already archived" if archived?
|
|
172
172
|
|
|
173
173
|
self.archived_at = Time.current
|
|
@@ -186,10 +186,10 @@ module BetterModel
|
|
|
186
186
|
# Ripristina record archiviato
|
|
187
187
|
#
|
|
188
188
|
# @return [self]
|
|
189
|
-
# @raise [
|
|
189
|
+
# @raise [ArchivableNotEnabledError] se archivable non รจ attivo
|
|
190
190
|
# @raise [NotArchivedError] se non archiviato
|
|
191
191
|
def restore!
|
|
192
|
-
raise
|
|
192
|
+
raise ArchivableNotEnabledError unless self.class.archivable_enabled?
|
|
193
193
|
raise NotArchivedError, "Record is not archived" unless archived?
|
|
194
194
|
|
|
195
195
|
self.archived_at = nil
|
|
@@ -241,7 +241,7 @@ module BetterModel
|
|
|
241
241
|
class AlreadyArchivedError < ArchivableError; end
|
|
242
242
|
class NotArchivedError < ArchivableError; end
|
|
243
243
|
|
|
244
|
-
class
|
|
244
|
+
class ArchivableNotEnabledError < ArchivableError
|
|
245
245
|
def initialize(msg = nil)
|
|
246
246
|
super(msg || "Archivable is not enabled. Add 'archivable do...end' to your model.")
|
|
247
247
|
end
|