better_model 2.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56752c6458384a8ebb0bf5eb0e6e02adbd8bc822ce817705cc85d0502abce8aa
4
- data.tar.gz: 8b8ce273a9b81d572f044154dd46486d1ad06086a7585b302875cd34069d28fc
3
+ metadata.gz: 3237879bac91f8200057cba591a1a2f09ccc761f55fd2ded1f1acb5a156c65f2
4
+ data.tar.gz: 6ab53b6128104cac44fdb5ad8ce80ca662a78072724a29c946c9380ad66701f2
5
5
  SHA512:
6
- metadata.gz: a509b30bdb21978f8f75cced201a5b4bc6bc77b96dfd7a28dba6242c3d8b274984d6bf7e1d2f73902d404fab47eda51edb7b67cd8fcca918e3f85f5ad745f9c9
7
- data.tar.gz: 69a04f82516a2de9bfa837091ee4823c7c4036f764d9cd8ad1e12615cdeac291929a0f491320c1a698fd2f9d6988e904716b7c08ad942d2683f16a4e99a3eb3c
6
+ metadata.gz: cf5e0a75c42e6f8c81f76756dd4f292779d1df7e258741bc19877665530a50363d7271c31fcace870d4dc61ac75e7dbd6bf01db3c28857fc29da8ab8e87cbed5
7
+ data.tar.gz: 61992d1b052d377e1db14d312ea3eb024998d2f0e36dcb031fd9e900a3bd35b0add348bfbfb6dc687c41b37ce9df85952924cbb5fb7942abd484af107ce87664
data/README.md CHANGED
@@ -55,11 +55,11 @@ class Article < ApplicationRecord
55
55
  # 6. VALIDATABLE - Declarative validation system (opt-in)
56
56
  validatable do
57
57
  # Basic validations
58
- validate :title, :content, presence: true
58
+ check :title, :content, presence: true
59
59
 
60
60
  # Conditional validations
61
61
  validate_if :is_published? do
62
- validate :published_at, presence: true
62
+ check :published_at, presence: true
63
63
  end
64
64
 
65
65
  # Cross-field validations
@@ -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
@@ -369,20 +339,23 @@ rails db:migrate
369
339
 
370
340
  BetterModel provides ten powerful concerns that work seamlessly together:
371
341
 
372
- ### Core Features
342
+ ### Core Features (Always Available)
373
343
 
374
344
  - **✨ Statusable** - Declarative status management with lambda-based conditions
375
345
  - **🔐 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
346
  - **⬆️ Sortable** - Type-aware sorting scopes
379
347
  - **🔍 Predicable** - Advanced filtering with rich predicate system
348
+
349
+ ### Opt-in Features (Require Activation)
350
+
380
351
  - **🔎 Searchable** - Unified search interface (Predicable + Sortable)
352
+ - **🗄️ Archivable** - Soft delete with tracking (by user, reason)
381
353
  - **✅ Validatable** - Declarative validation DSL with conditional rules
382
354
  - **🔄 Stateable** - Declarative state machines with guards & callbacks
355
+ - **⏰ Traceable** - Complete audit trail with time-travel and rollback
383
356
  - **🏷️ Taggable** 🆕 - Tag management with normalization, validation, and statistics
384
357
 
385
- [See all features in detail →](#-features)
358
+ [See all features in detail →](#feature-details)
386
359
 
387
360
  ## ⚙️ Requirements
388
361
 
@@ -406,9 +379,22 @@ BetterModel works with all databases supported by ActiveRecord:
406
379
  - Array predicates: `overlaps`, `contains`, `contained_by`
407
380
  - JSONB predicates: `has_key`, `has_any_key`, `has_all_keys`, `jsonb_contains`
408
381
 
409
- ## 📚 Features
382
+ ## 🗄️ Database Requirements
383
+
384
+ Some opt-in features require database columns. Use the provided generators to add them:
385
+
386
+ | Feature | Database Requirement | Generator Command |
387
+ |---------|---------------------|-------------------|
388
+ | **Archivable** | `archived_at` column | `rails g better_model:archivable Model` |
389
+ | **Stateable** | `state`, `transitions` columns | `rails g better_model:stateable Model` |
390
+ | **Traceable** | `version_records` table | `rails g better_model:traceable Model` |
391
+ | **Taggable** | `tags` JSONB/text column | `rails g better_model:taggable Model` |
410
392
 
411
- BetterModel provides eight powerful concerns that work together seamlessly:
393
+ **Core features** (Statusable, Permissible, Predicable, Sortable, Searchable, Validatable) require no database changes.
394
+
395
+ ## 📚 Feature Details
396
+
397
+ BetterModel provides ten powerful concerns that work together seamlessly:
412
398
 
413
399
  ### 📋 Statusable - Declarative Status Management
414
400
 
@@ -542,9 +528,75 @@ Generate comprehensive predicate scopes for filtering and searching with support
542
528
  - 📊 Range queries (between) for numerics and dates
543
529
  - 🐘 PostgreSQL array and JSONB support
544
530
  - 🔗 Chainable with standard ActiveRecord queries
531
+ - 🧩 Custom complex predicates for business logic
545
532
 
546
533
  **[📖 Full Documentation →](docs/predicable.md)**
547
534
 
535
+ #### 🧩 Complex Predicates
536
+
537
+ 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.
538
+
539
+ **Basic Example:**
540
+
541
+ ```ruby
542
+ class Article < ApplicationRecord
543
+ include BetterModel
544
+
545
+ predicates :title, :view_count, :published_at
546
+
547
+ # Define a complex predicate with parameters
548
+ register_complex_predicate :trending do |days = 7, min_views = 100|
549
+ where("published_at >= ? AND view_count >= ?", days.days.ago, min_views)
550
+ end
551
+
552
+ # Define a complex predicate for association queries
553
+ register_complex_predicate :popular_author do |min_articles = 10|
554
+ joins(:author)
555
+ .group("articles.author_id")
556
+ .having("COUNT(articles.id) >= ?", min_articles)
557
+ end
558
+ end
559
+ ```
560
+
561
+ **Usage:**
562
+
563
+ ```ruby
564
+ # Use with default parameters
565
+ Article.trending
566
+ # => Articles from last 7 days with 100+ views
567
+
568
+ # Use with custom parameters
569
+ Article.trending(14, 200)
570
+ # => Articles from last 14 days with 200+ views
571
+
572
+ # Chain with standard predicates
573
+ Article.trending(7, 100)
574
+ .title_cont("Ruby")
575
+ .status_eq("published")
576
+ .sort_view_count_desc
577
+
578
+ # Association-based queries
579
+ Article.popular_author(5)
580
+ .published_at_within(30.days)
581
+ ```
582
+
583
+ **When to Use Complex Predicates:**
584
+
585
+ | Standard Predicates ✅ | Complex Predicates 🧩 |
586
+ |------------------------|------------------------|
587
+ | Single field filtering | Multi-field conditions |
588
+ | `title_eq("Ruby")` | `trending(days, views)` |
589
+ | `view_count_gt(100)` | Association queries |
590
+ | `published_at_within(7.days)` | Business logic encapsulation |
591
+ | Simple comparisons | Custom SQL expressions |
592
+
593
+ **Check if Defined:**
594
+
595
+ ```ruby
596
+ Article.complex_predicate?(:trending) # => true
597
+ Article.complex_predicates_registry # => { trending: #<Proc> }
598
+ ```
599
+
548
600
  ---
549
601
 
550
602
  ### 🔎 Searchable - Unified Search Interface
@@ -559,9 +611,54 @@ Orchestrate Predicable and Sortable into a powerful, secure search interface wit
559
611
  - ⚙️ Default ordering configuration
560
612
  - 💪 Strong parameters integration
561
613
  - ✅ Type-safe validation of all parameters
614
+ - 🚀 Eager loading support with `includes:`, `preload:`, `eager_load:`
562
615
 
563
616
  **[📖 Full Documentation →](docs/searchable.md)**
564
617
 
618
+ #### 🔗 Eager Loading Associations
619
+
620
+ Optimize N+1 queries with built-in eager loading support:
621
+
622
+ ```ruby
623
+ # Single association (always use array syntax)
624
+ Article.search(
625
+ { status_eq: "published" },
626
+ includes: [:author]
627
+ )
628
+
629
+ # Multiple associations
630
+ Article.search(
631
+ { status_eq: "published" },
632
+ includes: [:author, :comments],
633
+ preload: [:tags]
634
+ )
635
+
636
+ # Nested associations
637
+ Article.search(
638
+ { status_eq: "published" },
639
+ includes: [{ author: :profile }]
640
+ )
641
+
642
+ # Complex mix of associations
643
+ Article.search(
644
+ { status_eq: "published" },
645
+ includes: [:tags, { author: :profile }, { comments: :user }]
646
+ )
647
+
648
+ # Combined with pagination and ordering
649
+ Article.search(
650
+ { status_eq: "published" },
651
+ pagination: { page: 1, per_page: 25 },
652
+ orders: [:sort_view_count_desc],
653
+ includes: [:author, :comments]
654
+ )
655
+ ```
656
+
657
+ **Strategies:**
658
+ - `includes:` - Smart loading (LEFT OUTER JOIN or separate queries)
659
+ - `preload:` - Separate queries (avoids JOIN ambiguity)
660
+ - `eager_load:` - Force LEFT OUTER JOIN (use with caution with default_order)
661
+
565
662
  ---
566
663
 
567
664
  ### 🏷️ Taggable - Tag Management with Statistics
@@ -582,147 +679,6 @@ Manage tags with automatic normalization, validation, and comprehensive statisti
582
679
 
583
680
  ---
584
681
 
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
682
  ## 📌 Changelog
727
683
 
728
684
  See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
@@ -748,22 +704,15 @@ Detailed documentation for each BetterModel concern:
748
704
  - [**Searchable**](docs/searchable.md) - Unified search interface
749
705
  - [**Validatable**](docs/validatable.md) - Declarative validation system
750
706
  - [**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
707
+ - [**Taggable**](docs/taggable.md) 🆕 - Flexible tag management with normalization
759
708
 
760
709
  ### 💡 Quick Links
761
710
 
762
- - [Installation](#-installation)
763
- - [Quick Start](#-quick-start)
764
- - [Features Overview](#-features-overview)
765
- - [Requirements](#%EF%B8%8F-requirements)
766
- - [Contributing](#-contributing)
711
+ - [Installation](#installation)
712
+ - [Quick Start](#quick-start)
713
+ - [Features Overview](#features-overview)
714
+ - [Requirements](#requirements)
715
+ - [Contributing](#contributing)
767
716
 
768
717
  ## 🤝 Contributing
769
718
 
@@ -792,6 +741,35 @@ We welcome contributions! Here's how you can help:
792
741
 
793
742
  ### 🔧 Development Setup
794
743
 
744
+ #### 🐳 Using Docker (Recommended)
745
+
746
+ The easiest way to get started is with Docker, which provides a consistent development environment:
747
+
748
+ ```bash
749
+ # Clone your fork
750
+ git clone https://github.com/YOUR_USERNAME/better_model.git
751
+ cd better_model
752
+
753
+ # One-time setup: build image and install dependencies
754
+ bin/docker-setup
755
+
756
+ # Run tests
757
+ bin/docker-test
758
+
759
+ # Run RuboCop
760
+ bin/docker-rubocop
761
+
762
+ # Open interactive shell for debugging
763
+ docker compose run --rm app sh
764
+
765
+ # Run any command in the container
766
+ docker compose run --rm app bundle exec [command]
767
+ ```
768
+
769
+ #### 💻 Local Setup (Without Docker)
770
+
771
+ If you prefer to use your local Ruby installation:
772
+
795
773
  ```bash
796
774
  # Clone your fork
797
775
  git clone https://github.com/YOUR_USERNAME/better_model.git
@@ -810,6 +788,11 @@ bundle exec rake test # Coverage report in coverage/index.html
810
788
  bundle exec rubocop
811
789
  ```
812
790
 
791
+ **Requirements:**
792
+ - Ruby 3.0+ (Ruby 3.3 recommended)
793
+ - Rails 8.1+
794
+ - SQLite3
795
+
813
796
  ### 📊 Test Coverage Notes
814
797
 
815
798
  The test suite runs on **SQLite** for performance and portability. Current coverage: **92.57%** (1507 / 1628 lines).
@@ -104,7 +104,6 @@ module BetterModel
104
104
  sort :archived_at unless sortable_field?(:archived_at)
105
105
 
106
106
  # Definisci gli scope alias (approccio ibrido)
107
- # v2.0.0: predicates require parameters
108
107
  scope :archived, -> { archived_at_present(true) }
109
108
  scope :not_archived, -> { archived_at_null(true) }
110
109