better_model 2.0.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 +274 -208
- data/lib/better_model/archivable.rb +203 -92
- 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 +142 -131
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +123 -96
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +103 -85
- data/lib/better_model/stateable/guard.rb +41 -21
- data/lib/better_model/stateable/transition.rb +64 -35
- data/lib/better_model/stateable.rb +43 -25
- 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 +54 -177
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -9
- 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 +45 -14
- data/lib/better_model/schedulable/occurrence_calculator.rb +0 -1034
- data/lib/better_model/schedulable/schedule_builder.rb +0 -269
- data/lib/better_model/schedulable.rb +0 -356
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -45
- 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
- data/lib/generators/better_model/taggable/taggable_generator.rb +0 -129
- data/lib/generators/better_model/taggable/templates/README.tt +0 -62
- data/lib/generators/better_model/taggable/templates/migration.rb.tt +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43311885d24f9c9001eba91737135c5fa8787cc42e5f5e7beaaf912e2ae177d0
|
|
4
|
+
data.tar.gz: cc80f44ca0c7c5992302175d949ef237d3b0f4d2274c9b1d13bba6398f6dad6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77874dd1ad5221c36a5498b74810f1812e978a984f24e35e50dcdfba0019899d4afe742bf431c68a3b5c94e30a3fe1e9c0f7d3e16e16722044ec4f91b2538b57
|
|
7
|
+
data.tar.gz: 11f774a0981cd7bc2ec1f307a63a8d0e3f91dc7431b58e262cd9bb5edc858e9548c8f51ea190e7e50a8b17c88965cdbc6d0bbb36e39e819fef9d7464ffb032de
|
data/README.md
CHANGED
|
@@ -53,20 +53,20 @@ class Article < ApplicationRecord
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# 6. VALIDATABLE - Declarative validation system (opt-in)
|
|
56
|
+
register_complex_validation :published_requirements do
|
|
57
|
+
return unless status == "published"
|
|
58
|
+
errors.add(:published_at, "must be present for published articles") if published_at.blank?
|
|
59
|
+
end
|
|
60
|
+
|
|
56
61
|
validatable do
|
|
57
62
|
# Basic validations
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Conditional validations
|
|
61
|
-
validate_if :is_published? do
|
|
62
|
-
validate :published_at, presence: true
|
|
63
|
-
end
|
|
63
|
+
check :title, :content, presence: true
|
|
64
64
|
|
|
65
|
-
#
|
|
66
|
-
|
|
65
|
+
# Conditional validations using Rails options
|
|
66
|
+
check :published_at, presence: true, if: -> { status == "published" }
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
|
|
68
|
+
# Complex validations for business rules
|
|
69
|
+
check_complex :published_requirements
|
|
70
70
|
|
|
71
71
|
# Validation groups (multi-step forms)
|
|
72
72
|
validation_group :step1, [:title, :content]
|
|
@@ -82,10 +82,10 @@ class Article < ApplicationRecord
|
|
|
82
82
|
|
|
83
83
|
# Define transitions with guards and callbacks
|
|
84
84
|
transition :publish, from: :draft, to: :published do
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
check { valid? }
|
|
86
|
+
check { title.present? && content.present? }
|
|
87
|
+
before_transition { self.published_at = Time.current }
|
|
88
|
+
after_transition { Rails.logger.info "Article #{id} published" }
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
transition :archive, from: [:draft, :published], to: :archived
|
|
@@ -307,34 +307,6 @@ rails db:migrate
|
|
|
307
307
|
- Optional metadata (JSON)
|
|
308
308
|
- Optimized indexes
|
|
309
309
|
|
|
310
|
-
### Taggable Generator
|
|
311
|
-
|
|
312
|
-
Add tags column with database-specific optimizations:
|
|
313
|
-
|
|
314
|
-
```bash
|
|
315
|
-
# Basic usage - adds tags column
|
|
316
|
-
rails g better_model:taggable Article
|
|
317
|
-
|
|
318
|
-
# Custom column name
|
|
319
|
-
rails g better_model:taggable Article --column-name=categories
|
|
320
|
-
|
|
321
|
-
# Skip GIN index (PostgreSQL only)
|
|
322
|
-
rails g better_model:taggable Article --skip-index
|
|
323
|
-
|
|
324
|
-
# Run migrations
|
|
325
|
-
rails db:migrate
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
**Generated migration includes:**
|
|
329
|
-
- **PostgreSQL**: Native array column (`array: true, default: []`) with GIN index for optimal performance
|
|
330
|
-
- **SQLite/MySQL**: Text column with serialization instructions
|
|
331
|
-
- Database-specific detection and optimization
|
|
332
|
-
- Automatic index creation for PostgreSQL (GIN index)
|
|
333
|
-
|
|
334
|
-
**Database-specific behavior:**
|
|
335
|
-
- PostgreSQL: Ready to use immediately with native array support
|
|
336
|
-
- SQLite/MySQL: Requires adding `serialize :tags, coder: JSON, type: Array` to model
|
|
337
|
-
|
|
338
310
|
### Generator Options
|
|
339
311
|
|
|
340
312
|
All generators support these common options:
|
|
@@ -350,13 +322,11 @@ All generators support these common options:
|
|
|
350
322
|
rails g better_model:traceable Article --create-table --pretend
|
|
351
323
|
rails g better_model:archivable Article --create-columns --pretend
|
|
352
324
|
rails g better_model:stateable Article --create-tables --pretend
|
|
353
|
-
rails g better_model:taggable Article --pretend
|
|
354
325
|
|
|
355
326
|
# 2. Generate for real
|
|
356
327
|
rails g better_model:traceable Article --create-table
|
|
357
328
|
rails g better_model:archivable Article --create-columns
|
|
358
329
|
rails g better_model:stateable Article --create-tables
|
|
359
|
-
rails g better_model:taggable Article
|
|
360
330
|
|
|
361
331
|
# 3. Run migrations
|
|
362
332
|
rails db:migrate
|
|
@@ -365,24 +335,111 @@ rails db:migrate
|
|
|
365
335
|
# See model setup instructions after running each generator
|
|
366
336
|
```
|
|
367
337
|
|
|
338
|
+
### Repository Generator
|
|
339
|
+
|
|
340
|
+
Create repository classes that implement the Repository Pattern:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Basic usage - creates model repository and ApplicationRepository
|
|
344
|
+
rails g better_model:repository Article
|
|
345
|
+
|
|
346
|
+
# Custom path
|
|
347
|
+
rails g better_model:repository Article --path app/services/repositories
|
|
348
|
+
|
|
349
|
+
# Skip ApplicationRepository creation
|
|
350
|
+
rails g better_model:repository Article --skip-base
|
|
351
|
+
|
|
352
|
+
# With namespace
|
|
353
|
+
rails g better_model:repository Article --namespace Admin
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Generated repository includes:**
|
|
357
|
+
- Repository class inheriting from `ApplicationRepository` (or `BetterModel::Repositable::BaseRepository` with `--skip-base`)
|
|
358
|
+
- Modern Ruby 3 endless method syntax: `def model_class = Article`
|
|
359
|
+
- Commented examples of custom query methods
|
|
360
|
+
- Integration with BetterModel's Searchable, Predicable, and Sortable features
|
|
361
|
+
- Auto-generated documentation of available predicates and sort scopes (if model uses BetterModel)
|
|
362
|
+
|
|
363
|
+
**ApplicationRepository (generated once):**
|
|
364
|
+
- Base class for all repositories in your application
|
|
365
|
+
- Inherits from `BetterModel::Repositable::BaseRepository`
|
|
366
|
+
- Can be customized with application-wide repository behaviors
|
|
367
|
+
|
|
368
|
+
**Example usage:**
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
# app/repositories/article_repository.rb
|
|
372
|
+
class ArticleRepository < ApplicationRepository
|
|
373
|
+
def model_class = Article
|
|
374
|
+
|
|
375
|
+
def published
|
|
376
|
+
search({ status_eq: "published" })
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def recent(days: 7)
|
|
380
|
+
search({ created_at_gteq: days.days.ago }, order_scope: { field: :created_at, direction: :desc })
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def popular(min_views: 100)
|
|
384
|
+
search({ view_count_gteq: min_views }, orders: [:sort_view_count_desc])
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def search_text(query)
|
|
388
|
+
search({
|
|
389
|
+
or: [
|
|
390
|
+
{ title_i_cont: query },
|
|
391
|
+
{ content_i_cont: query }
|
|
392
|
+
]
|
|
393
|
+
})
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# In your controllers/services
|
|
398
|
+
repo = ArticleRepository.new
|
|
399
|
+
articles = repo.published
|
|
400
|
+
popular = repo.popular(min_views: 200)
|
|
401
|
+
results = repo.search({ status_eq: "published" }, page: 1, per_page: 20)
|
|
402
|
+
article = repo.search({ id_eq: 1 }, limit: 1)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**BaseRepository features:**
|
|
406
|
+
- `search(predicates, page:, per_page:, includes:, joins:, order:, order_scope:, limit:)` - Main search method
|
|
407
|
+
- `find(id)`, `find_by(...)` - Standard ActiveRecord finders
|
|
408
|
+
- `create(attrs)`, `create!(attrs)` - Create records
|
|
409
|
+
- `build(attrs)` - Build new instances
|
|
410
|
+
- `update(id, attrs)` - Update records
|
|
411
|
+
- `delete(id)` - Delete records
|
|
412
|
+
- `where(...)`, `all`, `count`, `exists?` - Basic ActiveRecord methods
|
|
413
|
+
|
|
414
|
+
**Search method parameters:**
|
|
415
|
+
- **predicates**: Hash of BetterModel predicates (e.g., `{ status_eq: "published", view_count_gt: 100 }`)
|
|
416
|
+
- **page/per_page**: Pagination (default: page 1, 20 per page)
|
|
417
|
+
- **includes/joins**: Eager loading and associations
|
|
418
|
+
- **order**: SQL order clause
|
|
419
|
+
- **order_scope**: BetterModel sort scope (e.g., `{ field: :published_at, direction: :desc }`)
|
|
420
|
+
- **limit**: Result limit (Integer for limit, `:default` for pagination, `nil` for all results)
|
|
421
|
+
|
|
368
422
|
## 📋 Features Overview
|
|
369
423
|
|
|
370
424
|
BetterModel provides ten powerful concerns that work seamlessly together:
|
|
371
425
|
|
|
372
|
-
### Core Features
|
|
426
|
+
### Core Features (Always Available)
|
|
373
427
|
|
|
374
428
|
- **✨ Statusable** - Declarative status management with lambda-based conditions
|
|
375
429
|
- **🔐 Permissible** - State-based permission system
|
|
376
|
-
- **🗄️ Archivable** - Soft delete with tracking (by user, reason)
|
|
377
|
-
- **⏰ Traceable** - Complete audit trail with time-travel and rollback
|
|
378
430
|
- **⬆️ Sortable** - Type-aware sorting scopes
|
|
379
431
|
- **🔍 Predicable** - Advanced filtering with rich predicate system
|
|
432
|
+
|
|
433
|
+
### Opt-in Features (Require Activation)
|
|
434
|
+
|
|
380
435
|
- **🔎 Searchable** - Unified search interface (Predicable + Sortable)
|
|
436
|
+
- **🗄️ Archivable** - Soft delete with tracking (by user, reason)
|
|
381
437
|
- **✅ Validatable** - Declarative validation DSL with conditional rules
|
|
382
438
|
- **🔄 Stateable** - Declarative state machines with guards & callbacks
|
|
439
|
+
- **⏰ Traceable** - Complete audit trail with time-travel and rollback
|
|
383
440
|
- **🏷️ Taggable** 🆕 - Tag management with normalization, validation, and statistics
|
|
384
441
|
|
|
385
|
-
[See all features in detail →](
|
|
442
|
+
[See all features in detail →](#feature-details)
|
|
386
443
|
|
|
387
444
|
## ⚙️ Requirements
|
|
388
445
|
|
|
@@ -406,9 +463,22 @@ BetterModel works with all databases supported by ActiveRecord:
|
|
|
406
463
|
- Array predicates: `overlaps`, `contains`, `contained_by`
|
|
407
464
|
- JSONB predicates: `has_key`, `has_any_key`, `has_all_keys`, `jsonb_contains`
|
|
408
465
|
|
|
409
|
-
##
|
|
466
|
+
## 🗄️ Database Requirements
|
|
410
467
|
|
|
411
|
-
|
|
468
|
+
Some opt-in features require database columns. Use the provided generators to add them:
|
|
469
|
+
|
|
470
|
+
| Feature | Database Requirement | Generator Command |
|
|
471
|
+
|---------|---------------------|-------------------|
|
|
472
|
+
| **Archivable** | `archived_at` column | `rails g better_model:archivable Model` |
|
|
473
|
+
| **Stateable** | `state`, `transitions` columns | `rails g better_model:stateable Model` |
|
|
474
|
+
| **Traceable** | `version_records` table | `rails g better_model:traceable Model` |
|
|
475
|
+
| **Taggable** | `tags` JSONB/text column | `rails g better_model:taggable Model` |
|
|
476
|
+
|
|
477
|
+
**Core features** (Statusable, Permissible, Predicable, Sortable, Searchable, Validatable) require no database changes.
|
|
478
|
+
|
|
479
|
+
## 📚 Feature Details
|
|
480
|
+
|
|
481
|
+
BetterModel provides ten powerful concerns that work together seamlessly:
|
|
412
482
|
|
|
413
483
|
### 📋 Statusable - Declarative Status Management
|
|
414
484
|
|
|
@@ -464,11 +534,10 @@ Define validations declaratively with support for conditional rules, cross-field
|
|
|
464
534
|
|
|
465
535
|
**🎯 Key Benefits:**
|
|
466
536
|
- 🎛️ Opt-in activation: only enabled when explicitly configured
|
|
467
|
-
- ✨ Declarative DSL for
|
|
468
|
-
-
|
|
469
|
-
- 🔗 Cross-field validations: `validate_order` for date/number comparisons
|
|
470
|
-
- 💼 Business rules: delegate complex logic to custom methods
|
|
537
|
+
- ✨ Declarative DSL with `check` method for basic validations
|
|
538
|
+
- 🔗 Complex validations: reusable validation blocks for cross-field and business logic
|
|
471
539
|
- 📋 Validation groups: partial validation for multi-step forms
|
|
540
|
+
- 🔀 Conditional validations using Rails `if:` / `unless:` options
|
|
472
541
|
- 🔒 Thread-safe with immutable configuration
|
|
473
542
|
|
|
474
543
|
**[📖 Full Documentation →](docs/validatable.md)**
|
|
@@ -542,9 +611,75 @@ Generate comprehensive predicate scopes for filtering and searching with support
|
|
|
542
611
|
- 📊 Range queries (between) for numerics and dates
|
|
543
612
|
- 🐘 PostgreSQL array and JSONB support
|
|
544
613
|
- 🔗 Chainable with standard ActiveRecord queries
|
|
614
|
+
- 🧩 Custom complex predicates for business logic
|
|
545
615
|
|
|
546
616
|
**[📖 Full Documentation →](docs/predicable.md)**
|
|
547
617
|
|
|
618
|
+
#### 🧩 Complex Predicates
|
|
619
|
+
|
|
620
|
+
For queries that go beyond single-field filtering, you can register **complex predicates** - custom scopes that combine multiple conditions, work with associations, or encapsulate business logic.
|
|
621
|
+
|
|
622
|
+
**Basic Example:**
|
|
623
|
+
|
|
624
|
+
```ruby
|
|
625
|
+
class Article < ApplicationRecord
|
|
626
|
+
include BetterModel
|
|
627
|
+
|
|
628
|
+
predicates :title, :view_count, :published_at
|
|
629
|
+
|
|
630
|
+
# Define a complex predicate with parameters
|
|
631
|
+
register_complex_predicate :trending do |days = 7, min_views = 100|
|
|
632
|
+
where("published_at >= ? AND view_count >= ?", days.days.ago, min_views)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Define a complex predicate for association queries
|
|
636
|
+
register_complex_predicate :popular_author do |min_articles = 10|
|
|
637
|
+
joins(:author)
|
|
638
|
+
.group("articles.author_id")
|
|
639
|
+
.having("COUNT(articles.id) >= ?", min_articles)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**Usage:**
|
|
645
|
+
|
|
646
|
+
```ruby
|
|
647
|
+
# Use with default parameters
|
|
648
|
+
Article.trending
|
|
649
|
+
# => Articles from last 7 days with 100+ views
|
|
650
|
+
|
|
651
|
+
# Use with custom parameters
|
|
652
|
+
Article.trending(14, 200)
|
|
653
|
+
# => Articles from last 14 days with 200+ views
|
|
654
|
+
|
|
655
|
+
# Chain with standard predicates
|
|
656
|
+
Article.trending(7, 100)
|
|
657
|
+
.title_cont("Ruby")
|
|
658
|
+
.status_eq("published")
|
|
659
|
+
.sort_view_count_desc
|
|
660
|
+
|
|
661
|
+
# Association-based queries
|
|
662
|
+
Article.popular_author(5)
|
|
663
|
+
.published_at_within(30.days)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**When to Use Complex Predicates:**
|
|
667
|
+
|
|
668
|
+
| Standard Predicates ✅ | Complex Predicates 🧩 |
|
|
669
|
+
|------------------------|------------------------|
|
|
670
|
+
| Single field filtering | Multi-field conditions |
|
|
671
|
+
| `title_eq("Ruby")` | `trending(days, views)` |
|
|
672
|
+
| `view_count_gt(100)` | Association queries |
|
|
673
|
+
| `published_at_within(7.days)` | Business logic encapsulation |
|
|
674
|
+
| Simple comparisons | Custom SQL expressions |
|
|
675
|
+
|
|
676
|
+
**Check if Defined:**
|
|
677
|
+
|
|
678
|
+
```ruby
|
|
679
|
+
Article.complex_predicate?(:trending) # => true
|
|
680
|
+
Article.complex_predicates_registry # => { trending: #<Proc> }
|
|
681
|
+
```
|
|
682
|
+
|
|
548
683
|
---
|
|
549
684
|
|
|
550
685
|
### 🔎 Searchable - Unified Search Interface
|
|
@@ -559,9 +694,54 @@ Orchestrate Predicable and Sortable into a powerful, secure search interface wit
|
|
|
559
694
|
- ⚙️ Default ordering configuration
|
|
560
695
|
- 💪 Strong parameters integration
|
|
561
696
|
- ✅ Type-safe validation of all parameters
|
|
697
|
+
- 🚀 Eager loading support with `includes:`, `preload:`, `eager_load:`
|
|
562
698
|
|
|
563
699
|
**[📖 Full Documentation →](docs/searchable.md)**
|
|
564
700
|
|
|
701
|
+
#### 🔗 Eager Loading Associations
|
|
702
|
+
|
|
703
|
+
Optimize N+1 queries with built-in eager loading support:
|
|
704
|
+
|
|
705
|
+
```ruby
|
|
706
|
+
# Single association (always use array syntax)
|
|
707
|
+
Article.search(
|
|
708
|
+
{ status_eq: "published" },
|
|
709
|
+
includes: [:author]
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Multiple associations
|
|
713
|
+
Article.search(
|
|
714
|
+
{ status_eq: "published" },
|
|
715
|
+
includes: [:author, :comments],
|
|
716
|
+
preload: [:tags]
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# Nested associations
|
|
720
|
+
Article.search(
|
|
721
|
+
{ status_eq: "published" },
|
|
722
|
+
includes: [{ author: :profile }]
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# Complex mix of associations
|
|
726
|
+
Article.search(
|
|
727
|
+
{ status_eq: "published" },
|
|
728
|
+
includes: [:tags, { author: :profile }, { comments: :user }]
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# Combined with pagination and ordering
|
|
732
|
+
Article.search(
|
|
733
|
+
{ status_eq: "published" },
|
|
734
|
+
pagination: { page: 1, per_page: 25 },
|
|
735
|
+
orders: [:sort_view_count_desc],
|
|
736
|
+
includes: [:author, :comments]
|
|
737
|
+
)
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Strategies:**
|
|
741
|
+
- `includes:` - Smart loading (LEFT OUTER JOIN or separate queries)
|
|
742
|
+
- `preload:` - Separate queries (avoids JOIN ambiguity)
|
|
743
|
+
- `eager_load:` - Force LEFT OUTER JOIN (use with caution with default_order)
|
|
744
|
+
|
|
565
745
|
---
|
|
566
746
|
|
|
567
747
|
### 🏷️ Taggable - Tag Management with Statistics
|
|
@@ -582,147 +762,6 @@ Manage tags with automatic normalization, validation, and comprehensive statisti
|
|
|
582
762
|
|
|
583
763
|
---
|
|
584
764
|
|
|
585
|
-
### 📜 Traceable - Audit Trail & Change Tracking
|
|
586
|
-
|
|
587
|
-
Track all changes to your records with comprehensive audit trail functionality, time-travel queries, and rollback capabilities.
|
|
588
|
-
|
|
589
|
-
**🎯 Key Benefits:**
|
|
590
|
-
- 🎛️ Opt-in activation: only enabled when explicitly configured
|
|
591
|
-
- 🤖 Automatic change tracking on create/update/destroy
|
|
592
|
-
- ⏰ Time-travel: reconstruct record state at any point in time
|
|
593
|
-
- ↩️ Rollback: restore to previous versions
|
|
594
|
-
- 📝 Audit trail with who/why tracking
|
|
595
|
-
- 🔍 Query changes by user, date range, or field transitions
|
|
596
|
-
- 🗂️ Flexible table naming: per-model tables (default), shared table, or custom names
|
|
597
|
-
|
|
598
|
-
**[📖 Full Documentation →](docs/traceable.md)**
|
|
599
|
-
|
|
600
|
-
#### 🚀 Quick Setup
|
|
601
|
-
|
|
602
|
-
**1️⃣ Step 1: Create the versions table**
|
|
603
|
-
|
|
604
|
-
By default, each model gets its own versions table (`{model}_versions`):
|
|
605
|
-
|
|
606
|
-
```bash
|
|
607
|
-
# Creates migration for article_versions table
|
|
608
|
-
rails g better_model:traceable Article --create-table
|
|
609
|
-
rails db:migrate
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
Or use a custom table name:
|
|
613
|
-
|
|
614
|
-
```bash
|
|
615
|
-
# Creates migration for custom table name
|
|
616
|
-
rails g better_model:traceable Article --create-table --table-name=audit_log
|
|
617
|
-
rails db:migrate
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
**2️⃣ Step 2: Enable in your model**
|
|
621
|
-
|
|
622
|
-
```ruby
|
|
623
|
-
class Article < ApplicationRecord
|
|
624
|
-
include BetterModel
|
|
625
|
-
|
|
626
|
-
# Activate traceable (opt-in)
|
|
627
|
-
traceable do
|
|
628
|
-
track :status, :title, :published_at # Fields to track
|
|
629
|
-
# versions_table 'audit_log' # Optional: custom table (default: article_versions)
|
|
630
|
-
end
|
|
631
|
-
end
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
**💡 Usage:**
|
|
635
|
-
|
|
636
|
-
```ruby
|
|
637
|
-
# 🤖 Automatic tracking on changes
|
|
638
|
-
article.update!(status: "published", updated_by_id: user.id, updated_reason: "Approved")
|
|
639
|
-
|
|
640
|
-
# 🔍 Query version history
|
|
641
|
-
article.versions # All versions (ordered desc)
|
|
642
|
-
article.changes_for(:status) # Changes for specific field
|
|
643
|
-
article.audit_trail # Full formatted history
|
|
644
|
-
|
|
645
|
-
# ⏰ Time-travel: reconstruct state at specific time
|
|
646
|
-
past_article = article.as_of(3.days.ago)
|
|
647
|
-
past_article.status # => "draft" (what it was 3 days ago)
|
|
648
|
-
|
|
649
|
-
# ↩️ Rollback to previous version
|
|
650
|
-
version = article.versions.where(event: "updated").first
|
|
651
|
-
article.rollback_to(version, updated_by_id: user.id, updated_reason: "Mistake")
|
|
652
|
-
|
|
653
|
-
# 📊 Class-level queries
|
|
654
|
-
Article.changed_by(user.id) # Records changed by user
|
|
655
|
-
Article.changed_between(1.week.ago, Time.current) # Changes in period
|
|
656
|
-
Article.status_changed_from("draft").to("published") # Specific transitions (PostgreSQL)
|
|
657
|
-
|
|
658
|
-
# 📦 Integration with as_json
|
|
659
|
-
article.as_json(include_audit_trail: true) # Include full history in JSON
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
**💾 Database Schema:**
|
|
663
|
-
|
|
664
|
-
By default, each model gets its own versions table (e.g., `article_versions` for Article model).
|
|
665
|
-
You can also use a shared table across multiple models or a custom table name.
|
|
666
|
-
|
|
667
|
-
| Column | Type | Description |
|
|
668
|
-
|--------|------|-------------|
|
|
669
|
-
| `item_type` | string | Polymorphic model name |
|
|
670
|
-
| `item_id` | integer | Polymorphic record ID |
|
|
671
|
-
| `event` | string | Event type: created/updated/destroyed |
|
|
672
|
-
| `object_changes` | json | Before/after values for tracked fields |
|
|
673
|
-
| `updated_by_id` | integer | Optional: user who made the change |
|
|
674
|
-
| `updated_reason` | string | Optional: reason for the change |
|
|
675
|
-
| `created_at` | datetime | When the change occurred |
|
|
676
|
-
|
|
677
|
-
**🗂️ Table Naming Options:**
|
|
678
|
-
|
|
679
|
-
```ruby
|
|
680
|
-
# 1️⃣ Option 1: Per-model table (default)
|
|
681
|
-
class Article < ApplicationRecord
|
|
682
|
-
traceable do
|
|
683
|
-
track :status
|
|
684
|
-
# Uses article_versions table automatically
|
|
685
|
-
end
|
|
686
|
-
end
|
|
687
|
-
|
|
688
|
-
# 2️⃣ Option 2: Custom table name
|
|
689
|
-
class Article < ApplicationRecord
|
|
690
|
-
traceable do
|
|
691
|
-
track :status
|
|
692
|
-
versions_table 'audit_log' # Uses audit_log table
|
|
693
|
-
end
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
# 3️⃣ Option 3: Shared table across models
|
|
697
|
-
class Article < ApplicationRecord
|
|
698
|
-
traceable do
|
|
699
|
-
track :status
|
|
700
|
-
versions_table 'versions' # Shared table
|
|
701
|
-
end
|
|
702
|
-
end
|
|
703
|
-
|
|
704
|
-
class User < ApplicationRecord
|
|
705
|
-
traceable do
|
|
706
|
-
track :email
|
|
707
|
-
versions_table 'versions' # Same shared table
|
|
708
|
-
end
|
|
709
|
-
end
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
**📝 Optional Tracking:**
|
|
713
|
-
|
|
714
|
-
To track who made changes and why, simply set attributes before saving:
|
|
715
|
-
|
|
716
|
-
```ruby
|
|
717
|
-
article.updated_by_id = current_user.id
|
|
718
|
-
article.updated_reason = "Fixed typo"
|
|
719
|
-
article.update!(title: "Corrected Title")
|
|
720
|
-
|
|
721
|
-
# The version will automatically include updated_by_id and updated_reason
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
---
|
|
725
|
-
|
|
726
765
|
## 📌 Changelog
|
|
727
766
|
|
|
728
767
|
See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
|
|
@@ -748,22 +787,15 @@ Detailed documentation for each BetterModel concern:
|
|
|
748
787
|
- [**Searchable**](docs/searchable.md) - Unified search interface
|
|
749
788
|
- [**Validatable**](docs/validatable.md) - Declarative validation system
|
|
750
789
|
- [**Stateable**](docs/stateable.md) 🆕 - State machine with transitions
|
|
751
|
-
|
|
752
|
-
### 🎓 Advanced Guides
|
|
753
|
-
|
|
754
|
-
Learn how to master BetterModel in production:
|
|
755
|
-
|
|
756
|
-
- [**Integration Guide**](docs/integration_guide.md) 🆕 - Combining multiple concerns effectively
|
|
757
|
-
- [**Performance Guide**](docs/performance_guide.md) 🆕 - Optimization strategies and indexing
|
|
758
|
-
- [**Migration Guide**](docs/migration_guide.md) 🆕 - Adding BetterModel to existing apps
|
|
790
|
+
- [**Taggable**](docs/taggable.md) 🆕 - Flexible tag management with normalization
|
|
759
791
|
|
|
760
792
|
### 💡 Quick Links
|
|
761
793
|
|
|
762
|
-
- [Installation](
|
|
763
|
-
- [Quick Start](
|
|
764
|
-
- [Features Overview](
|
|
765
|
-
- [Requirements](
|
|
766
|
-
- [Contributing](
|
|
794
|
+
- [Installation](#installation)
|
|
795
|
+
- [Quick Start](#quick-start)
|
|
796
|
+
- [Features Overview](#features-overview)
|
|
797
|
+
- [Requirements](#requirements)
|
|
798
|
+
- [Contributing](#contributing)
|
|
767
799
|
|
|
768
800
|
## 🤝 Contributing
|
|
769
801
|
|
|
@@ -792,6 +824,35 @@ We welcome contributions! Here's how you can help:
|
|
|
792
824
|
|
|
793
825
|
### 🔧 Development Setup
|
|
794
826
|
|
|
827
|
+
#### 🐳 Using Docker (Recommended)
|
|
828
|
+
|
|
829
|
+
The easiest way to get started is with Docker, which provides a consistent development environment:
|
|
830
|
+
|
|
831
|
+
```bash
|
|
832
|
+
# Clone your fork
|
|
833
|
+
git clone https://github.com/YOUR_USERNAME/better_model.git
|
|
834
|
+
cd better_model
|
|
835
|
+
|
|
836
|
+
# One-time setup: build image and install dependencies
|
|
837
|
+
bin/docker-setup
|
|
838
|
+
|
|
839
|
+
# Run tests
|
|
840
|
+
bin/docker-test
|
|
841
|
+
|
|
842
|
+
# Run RuboCop
|
|
843
|
+
bin/docker-rubocop
|
|
844
|
+
|
|
845
|
+
# Open interactive shell for debugging
|
|
846
|
+
docker compose run --rm app sh
|
|
847
|
+
|
|
848
|
+
# Run any command in the container
|
|
849
|
+
docker compose run --rm app bundle exec [command]
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
#### 💻 Local Setup (Without Docker)
|
|
853
|
+
|
|
854
|
+
If you prefer to use your local Ruby installation:
|
|
855
|
+
|
|
795
856
|
```bash
|
|
796
857
|
# Clone your fork
|
|
797
858
|
git clone https://github.com/YOUR_USERNAME/better_model.git
|
|
@@ -810,6 +871,11 @@ bundle exec rake test # Coverage report in coverage/index.html
|
|
|
810
871
|
bundle exec rubocop
|
|
811
872
|
```
|
|
812
873
|
|
|
874
|
+
**Requirements:**
|
|
875
|
+
- Ruby 3.0+ (Ruby 3.3 recommended)
|
|
876
|
+
- Rails 8.1+
|
|
877
|
+
- SQLite3
|
|
878
|
+
|
|
813
879
|
### 📊 Test Coverage Notes
|
|
814
880
|
|
|
815
881
|
The test suite runs on **SQLite** for performance and portability. Current coverage: **92.57%** (1507 / 1628 lines).
|