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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +274 -208
  3. data/lib/better_model/archivable.rb +203 -92
  4. data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
  5. data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
  6. data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
  7. data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
  8. data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
  9. data/lib/better_model/errors/better_model_error.rb +9 -0
  10. data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
  11. data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
  12. data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
  13. data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
  14. data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
  15. data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
  16. data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
  17. data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
  18. data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
  19. data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
  20. data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
  21. data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
  22. data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
  23. data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
  24. data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
  25. data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
  26. data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
  27. data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
  28. data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
  29. data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
  30. data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
  31. data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
  32. data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
  33. data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
  34. data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
  35. data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
  36. data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
  37. data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
  38. data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
  39. data/lib/better_model/models/state_transition.rb +122 -0
  40. data/lib/better_model/models/version.rb +68 -0
  41. data/lib/better_model/permissible.rb +103 -52
  42. data/lib/better_model/predicable.rb +142 -131
  43. data/lib/better_model/repositable/base_repository.rb +232 -0
  44. data/lib/better_model/repositable.rb +32 -0
  45. data/lib/better_model/searchable.rb +123 -96
  46. data/lib/better_model/sortable.rb +137 -41
  47. data/lib/better_model/stateable/configurator.rb +103 -85
  48. data/lib/better_model/stateable/guard.rb +41 -21
  49. data/lib/better_model/stateable/transition.rb +64 -35
  50. data/lib/better_model/stateable.rb +43 -25
  51. data/lib/better_model/statusable.rb +84 -52
  52. data/lib/better_model/taggable.rb +120 -75
  53. data/lib/better_model/traceable.rb +56 -48
  54. data/lib/better_model/validatable/configurator.rb +54 -177
  55. data/lib/better_model/validatable.rb +88 -113
  56. data/lib/better_model/version.rb +1 -1
  57. data/lib/better_model.rb +42 -9
  58. data/lib/generators/better_model/repository/repository_generator.rb +141 -0
  59. data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
  60. data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
  61. data/lib/generators/better_model/stateable/templates/README +1 -1
  62. metadata +45 -14
  63. data/lib/better_model/schedulable/occurrence_calculator.rb +0 -1034
  64. data/lib/better_model/schedulable/schedule_builder.rb +0 -269
  65. data/lib/better_model/schedulable.rb +0 -356
  66. data/lib/better_model/state_transition.rb +0 -106
  67. data/lib/better_model/stateable/errors.rb +0 -45
  68. data/lib/better_model/validatable/business_rule_validator.rb +0 -47
  69. data/lib/better_model/validatable/order_validator.rb +0 -77
  70. data/lib/better_model/version_record.rb +0 -66
  71. data/lib/generators/better_model/taggable/taggable_generator.rb +0 -129
  72. data/lib/generators/better_model/taggable/templates/README.tt +0 -62
  73. 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: 56752c6458384a8ebb0bf5eb0e6e02adbd8bc822ce817705cc85d0502abce8aa
4
- data.tar.gz: 8b8ce273a9b81d572f044154dd46486d1ad06086a7585b302875cd34069d28fc
3
+ metadata.gz: 43311885d24f9c9001eba91737135c5fa8787cc42e5f5e7beaaf912e2ae177d0
4
+ data.tar.gz: cc80f44ca0c7c5992302175d949ef237d3b0f4d2274c9b1d13bba6398f6dad6c
5
5
  SHA512:
6
- metadata.gz: a509b30bdb21978f8f75cced201a5b4bc6bc77b96dfd7a28dba6242c3d8b274984d6bf7e1d2f73902d404fab47eda51edb7b67cd8fcca918e3f85f5ad745f9c9
7
- data.tar.gz: 69a04f82516a2de9bfa837091ee4823c7c4036f764d9cd8ad1e12615cdeac291929a0f491320c1a698fd2f9d6988e904716b7c08ad942d2683f16a4e99a3eb3c
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
- validate :title, :content, presence: true
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
- # Cross-field validations
66
- validate_order :starts_at, :before, :ends_at
65
+ # Conditional validations using Rails options
66
+ check :published_at, presence: true, if: -> { status == "published" }
67
67
 
68
- # Business rules
69
- validate_business_rule :valid_category
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
- guard { valid? }
86
- guard if: :is_ready_for_publishing? # Statusable integration
87
- before { set_published_at }
88
- after { notify_subscribers }
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 →](#-features)
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
- ## 📚 Features
466
+ ## 🗄️ Database Requirements
410
467
 
411
- BetterModel provides eight powerful concerns that work together seamlessly:
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 all validation types
468
- - 🔀 Conditional validations: `validate_if` / `validate_unless`
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](#-installation)
763
- - [Quick Start](#-quick-start)
764
- - [Features Overview](#-features-overview)
765
- - [Requirements](#%EF%B8%8F-requirements)
766
- - [Contributing](#-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).