=============== LIBRARY RULES =============== From library maintainers: - Include BetterModel in ActiveRecord models to enable all features at once with 'include BetterModel' - Each feature (Statusable, Permissible, Predicable, Sortable, Searchable) is automatically available when BetterModel is included - Opt-in features require explicit activation: Archivable (with archived_at column), Validatable (with configuration block), Stateable (with state and transitions columns), Traceable (with version_records table), Taggable (with tags JSONB column) - Use Rails generators to create required migrations: 'rails generate better_model:archivable MODEL', 'rails generate better_model:stateable MODEL', 'rails generate better_model:traceable MODEL', 'rails generate better_model:taggable MODEL' - PostgreSQL provides additional predicates: overlaps, contains, contained_by for arrays; has_key, has_value, has_path for JSONB columns - Statusable declarations: use 'is :status_name, -> { condition }' to define dynamic boolean statuses based on model state and attributes - Permissible declarations: use 'permit :action_name, -> { condition }' to define action permissions based on model state - State machines: use 'state_machine initial: :state_name' with 'state :name, from: [:state1], to: :state2' declarations - Searchable accepts: text fields for full-text search, exact fields for exact matches, range fields for numeric/date ranges, boolean fields, enum fields, and custom scopes - Change tracking: Traceable automatically tracks all changes to model attributes, storing versions with changes hash, whodunnit (user), and metadata - Always use transaction blocks when performing state transitions or updates that involve multiple records - The gem requires Rails 8.1+ and Ruby 3.0+ minimum ### Bundle Install Command Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md The command to run after adding the BetterModel gem to the Gemfile to download and install the gem and its dependencies. This is a fundamental step in managing project dependencies in Ruby. ```bash bundle install ``` -------------------------------- ### Basic BetterModel Configuration Setup Source: https://github.com/alessiobussolari/better_model/blob/main/context7/13_configuration.md Demonstrates the fundamental setup for configuring BetterModel using an initializer file. It shows how to set default values for searchable, traceable, stateable, and archivable modules, as well as global settings like strict mode and logger. ```ruby # config/initializers/better_model.rb BetterModel.configure do |config| # Searchable config.searchable_max_per_page = 100 config.searchable_default_per_page = 25 config.searchable_strict_predicates = false # Traceable config.traceable_default_table_name = nil # Stateable config.stateable_default_table_name = "state_transitions" # Archivable config.archivable_skip_archived_by_default = false # Global config.strict_mode = Rails.env.development? config.logger = Rails.logger end ``` -------------------------------- ### Install BetterModel Gem Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md Instructions for adding the BetterModel gem to your project's Gemfile and installing it using Bundler. This is the standard procedure for incorporating any Ruby gem into a Rails application. ```ruby gem 'better_model', '~> 3.0.0' ``` -------------------------------- ### Accessing, Resetting, and Exporting BetterModel Configuration Source: https://github.com/alessiobussolari/better_model/blob/main/context7/13_configuration.md Provides examples of how to interact with the current BetterModel configuration. It covers accessing specific configuration values, resetting all configurations back to their default values, and exporting the current configuration as a hash. ```ruby # Access current config BetterModel.configuration.searchable_max_per_page # => 100 # Reset to defaults BetterModel.reset_configuration! # Export as hash BetterModel.configuration.to_h ``` -------------------------------- ### Taggable Module Usage with PostgreSQL (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Provides an example of using the Taggable module with PostgreSQL, highlighting the need for a GIN index and demonstrating the `contains` query. Suggests counter cache for large datasets. ```ruby # Requires GIN index # CREATE INDEX idx_articles_tags ON articles USING GIN (tags) # Contains (uses index) Article.tags_contains("ruby") # For large datasets, consider counter cache ``` -------------------------------- ### Development Setup with Docker for Better Model Source: https://github.com/alessiobussolari/better_model/blob/main/README.md Provides instructions for setting up the development environment for the Better Model gem using Docker. This includes cloning the repository, building the Docker image, installing dependencies, and running tests within the Docker container. ```bash # Clone your fork git clone https://github.com/YOUR_USERNAME/better_model.git cd better_model # One-time setup: build image and install dependencies bin/docker-setup # Run tests bin/docker-test ``` -------------------------------- ### Configuring BetterModel Strict Mode Source: https://github.com/alessiobussolari/better_model/blob/main/context7/13_configuration.md Illustrates how to set the `strict_mode` configuration option for BetterModel. Strict mode controls whether errors are raised immediately or logged as warnings. Examples are provided for development and production environments. ```ruby # Development: raise errors immediately config.strict_mode = true # Production: log warnings config.strict_mode = false ``` -------------------------------- ### Searchable Module Configuration and Usage (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Demonstrates how to use the Searchable module with pagination and how to configure the global maximum per page setting. ```ruby # Always paginate Article.search( { status_eq: "published" }, page: 1, per_page: 25 ) # Configure max globally BetterModel.configure do |config| config.searchable_max_per_page = 100 end ``` -------------------------------- ### Environment-Specific BetterModel Configuration Source: https://github.com/alessiobussolari/better_model/blob/main/context7/13_configuration.md Shows how to apply environment-specific configurations to BetterModel. This example dynamically sets `strict_mode` based on the Rails environment and conditionally assigns the logger only in development. ```ruby BetterModel.configure do |config| config.strict_mode = !Rails.env.production? config.logger = Rails.logger if Rails.env.development? end ``` -------------------------------- ### Stateable Module Configuration and Usage (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Demonstrates configuring state transitions in the Stateable module, emphasizing lightweight attribute checks and avoiding expensive queries within checks. ```ruby # Keep checks lightweight transition :publish, from: :draft, to: :published do # Good - attribute check check { content.present? } # Avoid - expensive query # check { Article.published.count < 1000 } end ``` -------------------------------- ### Archivable Module Configuration and Usage (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Shows how to configure the Archivable module to skip archived records by default and how to use `unscoped` when necessary. ```ruby # Default scope adds WHERE clause archivable do skip_archived_by_default true # Adds: WHERE archived_at IS NULL end # Use unscoped when needed Article.unscoped.find(id) ``` -------------------------------- ### Basic Query Chaining Example Source: https://github.com/alessiobussolari/better_model/blob/main/context7/03_predicable.md Demonstrates a simple query using the User model with chaining of settings and date predicates. ```ruby User .settings_has_key('email_notifications') .settings_jsonb_contains({active: true}) .created_at_within(30.days) ``` -------------------------------- ### Memory Management Tips (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Provides advice on memory management in Ruby, explaining lazy scope evaluation and recommending limiting version history loads. ```ruby # Scopes are lazy (no query until needed) scope = Article.status_eq("published").sort_title_asc # Query executes here: scope.to_a # Limit version history article.versions.limit(20) article.versions.where("created_at > ?", 1.month.ago) ``` -------------------------------- ### Ruby: Best Practice - Descriptive State Names in Better Model Source: https://github.com/alessiobussolari/better_model/blob/main/context7/08_stateable.md Example demonstrating the use of clear, domain-specific state names for better readability and maintainability of state machines. Contrasts good examples with ambiguous ones. ```ruby # Good - clear business meaning stateable do state :pending_review state :approved_by_manager state :in_production state :delivered_to_customer end # Bad - ambiguous stateable do state :state1 state :state2 state :processing end ``` -------------------------------- ### Run Database Migrations Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md Executes all pending database migrations, including those generated for BetterModel opt-in features like archivable, stateable, traceable, and taggable. This command is essential for updating the database schema. ```bash rails db:migrate ``` -------------------------------- ### Add BetterModel Stateable Feature Migration Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md Generates a Rails migration to add `state` and `transitions` columns to a model, facilitating the stateable feature for managing object states and transitions. This is part of the opt-in feature setup requiring database modifications. ```bash # Stateable - adds state and transitions columns rails generate better_model:stateable Order ``` -------------------------------- ### Rails Integration for BetterModel Configuration Source: https://github.com/alessiobussolari/better_model/blob/main/context7/13_configuration.md Demonstrates two primary methods for integrating BetterModel configuration into a Rails application. The first shows configuration directly within `config/application.rb`, while the second, recommended approach uses an initializer file. ```ruby # Via Rails config (config/application.rb) config.better_model.strict_mode = true # Via initializer (recommended) BetterModel.configure do |config| config.strict_mode = true end ``` -------------------------------- ### Stateable Module Usage and Examples Source: https://context7.com/alessiobussolari/better_model/llms.txt Provides examples of how to use the Stateable module after defining state machines. It shows how to create records, check current states, verify transition possibilities, execute transitions, and access transition history. Includes JSON serialization with transition history. ```ruby # Usage order = Order.create!(customer: customer, items: [item1, item2]) order.state # => "pending" order.pending? # => true (auto-generated method) # Check if transition is possible order.can_confirm? # => true (checks all guards) order.can_pay? # => false (not in :confirmed state) # Execute transition order.confirm! # Runs guards → validations → before callbacks → state change → after callbacks → save! order.state # => "confirmed" order.confirmed? # => true # Transition with metadata order.pay!(payment_method: "stripe", transaction_id: "txn_123") order.state # => "paid" # Access transition history order.state_transitions.count # => 2 order.state_transitions.first # => # order.transition_history # => [ # { event: "pay", from: "confirmed", to: "paid", at: 2025-11-03 10:31:00 UTC, metadata: {...} }, # { event: "confirm", from: "pending", to: "confirmed", at: 2025-11-03 10:30:00 UTC, metadata: nil } # ] # JSON serialization order.as_json(include_transition_history: true) # => { # "id" => 1, # "state" => "paid", # "transition_history" => [...] # } ``` -------------------------------- ### Include BetterModel Core Features in Rails Model Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md Demonstrates how to include the BetterModel module in a Rails model to gain access to its core features like Statusable, Permissible, Predicable, and Sortable. This is the basic setup for using BetterModel. ```ruby class Product < ApplicationRecord include BetterModel # Now you have access to core features: # - Statusable (status declarations) # - Permissible (permission declarations) # - Predicable (dynamic predicates) # - Sortable (flexible sorting) # Opt-in features require explicit activation: # predicates :name, :price, :stock # Configure first # sort :name, :price # Configure sorting # searchable do # Then activate search # per_page 25 # max_per_page 100 # end end ``` -------------------------------- ### Traceable Module Configuration and Usage (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Configures the Traceable module to track essential fields and ignore frequently changing ones. Includes a tip on limiting version history loads. ```ruby # Track only essential fields traceable do track :title, :status, :content ignore :view_count, :updated_at # Skip frequently changing end # Limit version history loads article.versions.limit(20) # Not article.versions.to_a ``` -------------------------------- ### Business Rule Predicates Examples (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/03_predicable.md Provides examples of registering complex predicates that encapsulate specific business rules, such as checking stock, sale status, low stock alerts, and price range conversions. ```ruby class Product < ApplicationRecord include BetterModel predicates :name, :price, :stock, :sale_price # In stock check register_complex_predicate :in_stock do where("stock > 0") end # On sale logic register_complex_predicate :on_sale do where("sale_price IS NOT NULL AND sale_price < price") end # Low stock alert register_complex_predicate :low_stock do |threshold = 10| where("stock > 0 AND stock <= ?", threshold) end # Price range with conversion register_complex_predicate :price_range_usd do |min, max, rate = 1.0| where("price * ? BETWEEN ? AND ?", rate, min, max) end end # Usage Product.in_stock Product.on_sale Product.low_stock(5) Product.price_range_usd(10, 100, 1.2) # With exchange rate ``` -------------------------------- ### Creating Database Indexes for BetterModel (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md A Ruby on Rails migration script demonstrating how to add various indexes to support BetterModel's features like Predicable, Sortable, Archivable, Stateable, Traceable, and Taggable modules. ```ruby # migration class AddBetterModelIndexes < ActiveRecord::Migration[7.0] def change # Predicable/Searchable add_index :articles, :status add_index :articles, :published_at # Sortable add_index :articles, :created_at # Archivable add_index :articles, :archived_at # Stateable add_index :articles, :state add_index :state_transitions, [:transitionable_type, :transitionable_id] # Traceable add_index :article_versions, [:article_id, :created_at] # Taggable (PostgreSQL) execute "CREATE INDEX idx_articles_tags ON articles USING GIN (tags)" # Composite for common searches add_index :articles, [:status, :published_at] end end ``` -------------------------------- ### Query Optimization Techniques (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Illustrates query optimization techniques in Ruby, including using `includes` for associations, batch processing with `find_each`, and using `pluck` for efficient data retrieval. ```ruby # Use includes for associations Article.search(status_eq: "published") .includes(:author) # Batch processing Article.archived.find_each(batch_size: 1000) do |article| article.restore! end # Pluck for simple data Article.search(status_eq: "published").pluck(:title) ``` -------------------------------- ### Caching Strategies (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Demonstrates caching strategies in Ruby using `Rails.cache` for search results and tag statistics, including setting expiration times. ```ruby # Cache search results @articles = Rails.cache.fetch("articles/#{search_params}/#{page}", expires_in: 5.minutes) do Article.search(search_params, page: page).to_a end # Cache tag stats def popular_tags Rails.cache.fetch("articles/popular_tags", expires_in: 1.hour) do Article.popular_tags(limit: 20) end end ``` -------------------------------- ### Complete Product Model with Chained Sorting Source: https://github.com/alessiobussolari/better_model/blob/main/context7/04_sortable.md This Ruby example shows a comprehensive Product model setup using BetterModel. It declares sortable fields and demonstrates how to register complex sorts and chain multiple sorting orders, including NULL handling and ActiveRecord conditions. This is useful for e-commerce or inventory systems. ```ruby class Product < ApplicationRecord include BetterModel # Declare sortable fields sort :name, :price, :stock, :created_at, :rating # Optional: register complex sorts register_complex_sort :best_value do order('rating / NULLIF(price, 0) DESC NULLS LAST') end end # Basic sorting Product.sort_name_asc # A-Z by name Product.sort_name_desc # Z-A by name Product.sort_name_asc_i # Case-insensitive A-Z Product.sort_price_asc # Cheapest first Product.sort_price_desc # Most expensive first # Chaining multiple sort orders # Primary: by price (cheapest first) # Secondary: by name (alphabetical for same price) Product.sort_price_asc.sort_name_asc_i # SQL: ORDER BY price ASC, LOWER(name) ASC # Three-level chain # Primary: by category (alphabetical) # Secondary: by price (cheapest) # Tertiary: by name (alphabetical) Product .sort_category_asc_i .sort_price_asc .sort_name_asc_i # SQL: ORDER BY LOWER(category) ASC, price ASC, LOWER(name) ASC # With NULL handling for optional price Product.sort_price_asc_nulls_last.sort_name_asc_i # Items without price appear at the end # Combining with ActiveRecord conditions Product .where('stock > 0') .where(active: true) .sort_price_asc .sort_name_asc_i .limit(50) ``` -------------------------------- ### Rake Task Output Examples Source: https://github.com/alessiobussolari/better_model/blob/main/context7/14_rails_integration.md Examples of the output produced by various BetterModel Rake tasks, illustrating configuration details, model usage summaries, health check results, and state distribution statistics. ```text === BetterModel Configuration === Searchable: max_per_page: 100 default_per_page: 25 Stateable: default_table_name: state_transitions Global: strict_mode: true logger: ActiveSupport::Logger === BetterModel Module Usage === Article: - Searchable predicates: 5 - Stateable states: 3 events: 2 - Traceable enabled: true Total: 1 models using BetterModel === BetterModel Health Check === ✓ Strict mode is enabled ✓ Logger is configured ✓ State transitions table 'state_transitions' exists ✓ All health checks passed === BetterModel State Distribution === Article (status): draft 12 (24.0%) ████ published 35 (70.0%) ██████████████ archived 3 ( 6.0%) █ ``` -------------------------------- ### Add BetterModel Traceable Feature Migration Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md Generates a Rails migration to set up the `version_records` table, enabling the traceable feature for comprehensive audit trails and version history. This command is used for opt-in features that require a dedicated table. ```bash # Traceable - creates version_records table rails generate better_model:traceable User ``` -------------------------------- ### Sortable Module Configuration and Usage (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Configures the Sortable module, defining fields for sorting. Notes differences in NULL handling across databases like PostgreSQL, SQLite, and MySQL. ```ruby # Good sort :title, :published_at # Memory: 4-6 scopes per field # NULL handling varies by DB: # - PostgreSQL/SQLite: native NULLS LAST # - MySQL: emulated with CASE ``` -------------------------------- ### Add BetterModel Repositable Feature Generator Source: https://github.com/alessiobussolari/better_model/blob/main/context7/README.md Generates a repository class for a model, supporting the repositable feature for clean architecture and data access abstraction. This command is used for opt-in features that involve creating associated classes. ```bash # Repositable - creates repository class rails generate better_model:repository Article ``` -------------------------------- ### Define Time-Based Query Methods in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Illustrates creating repository methods for time-based filtering, such as finding recent or trending articles. These examples use date comparisons and include parameters for flexibility. ```ruby class ArticleRepository < ApplicationRepository def model_class = Article def recent(days: 7) search( { status_eq: "published", published_at_gteq: days.days.ago }, order_scope: { field: :published_at, direction: :desc } ) end def trending(days: 7, min_views: 100) search( { status_eq: "published", published_at_gteq: days.days.ago, view_count_gteq: min_views }, order_scope: { field: :view_count, direction: :desc } ) endend> # Usage repo = ArticleRepository.new recent_articles = repo.recent(days: 30) trending_articles = repo.trending(days: 7, min_views: 500) ``` -------------------------------- ### Ruby: Clear Error Messages in Validations Source: https://github.com/alessiobussolari/better_model/blob/main/context7/07_validatable.md Provides examples of writing clear and actionable error messages in Ruby validations using `errors.add` with the Better Model gem. Specific messages guide users toward correcting issues effectively. ```ruby # Good - specific and actionable errors.add(:sale_price, "must be less than regular price ($#{price})") errors.add(:starts_at, "must be at least 24 hours from now") # Bad - vague messages errors.add(:sale_price, "invalid") errors.add(:starts_at, "wrong") ``` -------------------------------- ### Install BetterModel Gem Source: https://github.com/alessiobussolari/better_model/blob/main/README.md Instructions for adding the BetterModel gem to a Rails application's Gemfile and installing it via Bundler or directly. ```ruby gem "better_model" $ bundle install ``` ```bash $ gem install better_model ``` -------------------------------- ### Get Tag Statistics Source: https://github.com/alessiobussolari/better_model/blob/main/README.md This snippet shows how to retrieve statistics related to article tags. It includes getting a count of articles for each tag, finding the most popular tags, and identifying related tags. ```ruby Article.tag_counts # => {"ruby" => 45, "rails" => 38} Article.popular_tags(limit: 10) # => [["ruby", 45], ["rails", 38]] Article.related_tags("ruby", limit: 5) # => ["rails", "gem", "tutorial"] ``` -------------------------------- ### Perform Search with Pagination Options (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Illustrates how to apply pagination to search queries using `page` and `per_page` parameters. It covers default pagination values and custom settings, as well as how to access metadata about the pagination results, such as `current_page`, `total_pages`, and `total_count`. ```ruby repo = ArticleRepository.new # Default pagination (page 1, 20 per page) repo.search({ status_eq: "published" }) # Custom pagination repo.search( { status_eq: "published" }, page: 2, per_page: 50 ) # Access pagination metadata results = repo.search({ status_eq: "published" }, page: 1, per_page: 25) results.current_page # => 1 results.total_pages # => 4 results.total_count # => 100 ``` -------------------------------- ### PostgreSQL Database Setup for Tagging Source: https://github.com/alessiobussolari/better_model/blob/main/context7/10_taggable.md Sets up a PostgreSQL database with an array column for tags and a GIN index for efficient searching. This method is recommended for production environments requiring high performance. It requires a Rails migration to add the column and index. ```ruby class AddTagsToArticles < ActiveRecord::Migration[8.1] def change add_column :articles, :tags, :string, array: true, default: [] add_index :articles, :tags, using: :gin end end class Article < ApplicationRecord include BetterModel taggable do tag_field :tags normalize true end end ``` -------------------------------- ### SQLite/MySQL Database Setup with JSON Serialization for Tagging Source: https://github.com/alessiobussolari/better_model/blob/main/context7/10_taggable.md Configures SQLite or MySQL databases to store tags as a JSON array within a text column. This approach is suitable for development, simpler setups, or when PostgreSQL is not available. It requires a Rails migration and model serialization configuration. ```ruby class AddTagsToArticles < ActiveRecord::Migration[8.1] def change add_column :articles, :tags, :text end end class Article < ApplicationRecord include BetterModel serialize :tags, coder: JSON, type: Array taggable do tag_field :tags normalize true end end ``` -------------------------------- ### Rescuing Specific Errors in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/12_errors.md Demonstrates the best practice of rescuing specific errors rather than general ones. This allows for more granular error handling and targeted user feedback or logging. It contrasts a good example with specific rescues against a bad example with a blanket rescue. ```ruby # Good: begin Article.search(params) rescue BetterModel::Errors::Searchable::InvalidPredicateError => e render json: { error: "Invalid parameter: #{e.message}" }, status: :bad_request rescue BetterModel::Errors::Searchable::InvalidSecurityError => e render json: { error: "Unauthorized" }, status: :forbidden end # Bad: begin Article.search(params) rescue => e render json: { error: "Error" }, status: :internal_server_error end ``` -------------------------------- ### Create Records with Ruby Repository Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Shows how to create new records using a repository pattern. It covers creating records with attributes, creating with validation that raises on error, and building a record in memory without immediate saving. ```ruby repo = ArticleRepository.new # Create with attributes article = repo.create( title: "Rails Guide", content: "...", status: "draft" ) # Create with validation (raises on error) article = repo.create!( title: "Rails Guide", content: "...", status: "draft" ) # Build without saving article = repo.build(title: "Draft Article") article.content = "..." article.save ``` -------------------------------- ### Validate Transitions with Custom Logic in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/08_stateable.md This example showcases the `validate_transition` block for grouping complex, multi-field validations within a state machine event. It allows defining custom validation rules that must pass before a transition can occur. The first part shows the 'Good' practice of using `validate_transition` for coherent checks, including a custom profanity check. The 'Bad' example illustrates scattered, less maintainable checks using the `check` method. ```ruby # Good - grouped validations event :submit do transition from: :draft, to: :submitted validate_transition do errors.add(:title, "required") if title.blank? errors.add(:content, "too short") if content.length < 100 errors.add(:category, "required") if category_id.blank? if contains_profanity?(content) errors.add(:content, "contains inappropriate language") end end end # Bad - scattered checks event :submit do transition from: :draft, to: :submitted check -> { title.present? }, "Title required" check -> { content.length >= 100 }, "Content too short" check -> { category_id.present? }, "Category required" # Profanity check missing end ``` -------------------------------- ### Enable and Track State Transitions Source: https://github.com/alessiobussolari/better_model/blob/main/context7/08_stateable.md This example shows how to enable and track all state transitions for a model. By adding `track_transitions true` and setting up the `has_many` association for `state_transitions`, the gem automatically records each state change, providing an audit trail. ```ruby # After running: rails g better_model:stateable_history Order class Order < ApplicationRecord include BetterModel has_many :state_transitions, class_name: 'OrderStateTransition', dependent: :destroy stateable do track_transitions true # Enable history state :pending, initial: true state :confirmed state :shipped state :delivered event :confirm do transition from: :pending, to: :confirmed end event :ship do transition from: :confirmed, to: :shipped end end end # Usage order = Order.create! order.confirm! order.ship! # View history order.state_transitions # => [ # { from_state: "pending", to_state: "confirmed", event: "confirm", created_at: ... }, # { from_state: "confirmed", to_state: "shipped", event: "ship", created_at: ... } # ] ``` -------------------------------- ### Store Transition Metadata with Better Model in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/08_stateable.md This snippet demonstrates how to track transitions and store additional metadata using the Better Model gem in Ruby. It shows how to define states, events, and use the `after_transition` callback to update transition records with custom data like user ID, IP address, and timestamps. The example includes querying the stored metadata. ```ruby class Order < ApplicationRecord include BetterModel stateable do track_transitions true state :pending, initial: true state :confirmed event :confirm do transition from: :pending, to: :confirmed after_transition do |transition| # Store metadata transition.update(metadata: { user_id: Current.user&.id, ip_address: Current.ip, confirmed_at: Time.current, payment_method: payment_method }) end end end end # Query history with metadata order.state_transitions.find_by(event: 'confirm').metadata # => { user_id: 123, ip_address: "192.168.1.1", confirmed_at: "2025-11-11 10:30:00", ... } ``` -------------------------------- ### Preview Model Migrations with --pretend Option Source: https://github.com/alessiobussolari/better_model/blob/main/README.md This command demonstrates how to preview the migrations that would be generated for traceable, archivable, and stateable models without actually creating or modifying any files. It's useful for understanding the impact of generator options. ```bash # 1. Generate migrations (dry-run first to preview) rails g better_model:traceable Article --create-table --pretend rails g better_model:archivable Article --create-columns --pretend rails g better_model:stateable Article --create-tables --pretend ``` -------------------------------- ### Define User Statuses with last_login_at in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/01_statusable.md This Ruby example demonstrates defining various user statuses like 'active', 'inactive', 'recently_active', 'dormant', 'at_risk', 'never_logged_in', and 'vip' within a User model using BetterModel's Statusable. The statuses are computed based on the 'last_login_at' timestamp and other attributes, offering dynamic status management without database changes. It includes usage examples for checking individual statuses, retrieving all statuses, and filtering users. ```ruby class User < ApplicationRecord include BetterModel # Active status: logged in within the last 30 days is :active, -> { last_login_at && last_login_at >= 30.days.ago } # Inactive: no login for 30+ days is :inactive, -> { last_login_at.nil? || last_login_at < 30.days.ago } # Recently active: logged in within 24 hours is :recently_active, -> { last_login_at && last_login_at >= 24.hours.ago } # Dormant: no login for 90+ days is :dormant, -> { last_login_at.nil? || last_login_at < 90.days.ago } # At risk of churn: active but engagement declining is :at_risk, -> { last_login_at && last_login_at.between?(14.days.ago, 30.days.ago) } # Never logged in (new account) is :never_logged_in, -> { last_login_at.nil? } # VIP: premium tier AND active is :vip, -> { tier == "premium" && is?(:active) } end # Usage examples user = User.find(1) # Check activity status user.is_active? # => true (if logged in within 30 days) user.is_inactive? # => false user.is_recently_active? # => true (if logged in within 24h) user.is_dormant? # => false user.is_at_risk? # => false user.is_never_logged_in? # => false # Get all statuses user.statuses # => { # active: true, # inactive: false, # recently_active: true, # dormant: false, # at_risk: false, # never_logged_in: false, # vip: false # } # Use in conditional logic if user.is_dormant? UserMailer.reengagement_email(user).deliver_later elsif user.is_at_risk? UserMailer.retention_offer(user).deliver_later end # Filter users by status all_users = User.all active_users = all_users.select(&:is_active?) dormant_users = all_users.select(&:is_dormant?) ``` -------------------------------- ### Multi-Tenant Repository in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Implements automatic tenant scoping for queries in multi-tenant SaaS applications. It includes a base `TenantAwareRepository` that ensures all queries are scoped to the correct tenant, and an example `ArticleRepository` inheriting from it. Requires `ApplicationRepository`, `Article` model, and a `current_tenant` helper. ```ruby # Base tenant-aware repository class TenantAwareRepository < ApplicationRepository def initialize(tenant_id) super() @tenant_id = tenant_id end def search(predicates = {}, **options) predicates = predicates.merge(tenant_id_eq: @tenant_id) super(predicates, **options) end def create(attributes) super(attributes.merge(tenant_id: @tenant_id)) end end # Model-specific repository class ArticleRepository < TenantAwareRepository def model_class = Article def published search({ status_eq: "published" }) end end # Usage tenant_repo = ArticleRepository.new(current_tenant.id) articles = tenant_repo.published # Automatically scoped to tenant new_article = tenant_repo.create(title: "Test", content: "...") ``` -------------------------------- ### Example Usage of BetterModel Features Source: https://github.com/alessiobussolari/better_model/blob/main/README.md Illustrates how to interact with the features provided by BetterModel after inclusion, such as checking statuses using `is?` and accessing a hash of all statuses. ```ruby # ✅ Check statuses article.is?(:draft) # => true/false article.is_published? # => true/false article.statuses # => { draft: true, published: false, ... } ``` -------------------------------- ### Perform Basic Search with Predicates (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Demonstrates how to use the `search` method with BetterModel's predicate syntax for filtering records. It shows basic equality checks and combining multiple predicates using `and` (implicit) and `or` conditions. This is useful for creating targeted queries. ```ruby class ArticleRepository < ApplicationRepository def model_class = Article end repo = ArticleRepository.new # Basic search repo.search({ status_eq: "published" }) # Multiple predicates repo.search({ status_eq: "published", created_at_gteq: 7.days.ago }) # With OR conditions repo.search({ or: [ { status_eq: "published" }, { status_eq: "featured" } ] }) ``` -------------------------------- ### Execute Callbacks Before and After State Transitions Source: https://github.com/alessiobussolari/better_model/blob/main/context7/08_stateable.md This code demonstrates how to define `before_transition` and `after_transition` callbacks for state changes. These blocks allow you to execute custom logic, such as updating inventory or sending notifications, immediately before or after a state transition occurs. ```ruby class Order < ApplicationRecord include BetterModel stateable do state :pending, initial: true state :confirmed state :shipped state :delivered event :confirm do transition from: :pending, to: :confirmed before_transition do # Reserve stock order_items.each { |item| item.product.reserve_stock(item.quantity) } end after_transition do # Send confirmation email OrderMailer.confirmation(self).deliver_later # Log event Rails.logger.info "Order ##{id} confirmed at #{Time.current}" end end event :ship do transition from: :confirmed, to: :shipped after_transition do # Send shipping notification OrderMailer.shipped(self).deliver_later # Update inventory order_items.each { |item| item.product.decrement_stock!(item.quantity) } end end end end ``` -------------------------------- ### Controller Error Handling for Archiving in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/12_errors.md This Ruby controller example handles the case where an article is already archived during an archive operation. ```ruby class ArticlesController < ApplicationController # ... other actions ... def archive @article = Article.find(params[:id]) @article.archive!(reason: params[:reason], by: current_user) redirect_to articles_path, notice: "Article archived" rescue BetterModel::Errors::Archivable::AlreadyArchivedError redirect_to @article, notice: "Article is already archived" end # ... other actions ... end ``` -------------------------------- ### Basic Complex Predicate Registration (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/03_predicable.md Illustrates how to register custom complex predicates, both with and without parameters, using the `register_complex_predicate` method. Examples include 'trending' and 'recent_popular'. ```ruby class Article < ApplicationRecord include BetterModel predicates :view_count, :published_at, :status # No parameters register_complex_predicate :trending do where("view_count >= ? AND published_at >= ?", 1000, 24.hours.ago) end # With parameters register_complex_predicate :recent_popular do |days = 7, min_views = 100| where("published_at >= ? AND view_count >= ?", days.days.ago, min_views) end end # Usage Article.trending # Default logic Article.recent_popular # Default params (7 days, 100 views) Article.recent_popular(14, 500) # Custom params (14 days, 500 views) # Chainable with other predicates Article .recent_popular(7, 100) .status_eq("published") .featured_eq(true) ``` -------------------------------- ### Predicable Module Configuration and Usage (Ruby) Source: https://github.com/alessiobussolari/better_model/blob/main/context7/15_performance.md Configures the Predicable module to include specific fields for efficient data retrieval. Demonstrates optimized queries using indexed fields versus slower LIKE queries and shows faster LIKE alternatives. ```ruby # Good - specific fields only predicates :title, :status, :published_at # Memory: ~10-15 scopes per field # Fast (with index) Article.status_eq("published") # Slower (LIKE queries) Article.title_cont("Rails") # LIKE '%Rails%' # Faster LIKE Article.title_start("Rails") # LIKE 'Rails%' ``` -------------------------------- ### BetterModel Console Usage Source: https://github.com/alessiobussolari/better_model/blob/main/context7/14_rails_integration.md Examples of interacting with BetterModel features directly within the Rails console. This includes checking the current configuration and querying specific capabilities of models integrated with BetterModel. ```ruby # Rails console rails c # Check config BetterModel.configuration.to_h # Check model capabilities Article.searchable_predicates Article.sortable_fields Article.state_machine_config Article.traceable_enabled? ``` -------------------------------- ### E-commerce Product Repository in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Manages product catalog and inventory for e-commerce applications. It provides methods to retrieve active products, low stock items, on-sale products, and update stock quantities. Dependencies include `ApplicationRepository` and a `Product` model. ```ruby class ProductRepository < ApplicationRepository def model_class = Product def active search({ status_eq: "active", stock_quantity_gt: 0 }) end def low_stock(threshold: 10) search({ status_eq: "active", stock_quantity_lteq: threshold, stock_quantity_gt: 0 }) end def on_sale search({ status_eq: "active", sale_price_present: true }) end def update_stock(product_id, quantity_change) product = find(product_id) new_quantity = product.stock_quantity + quantity_change raise ArgumentError, "Insufficient stock" if new_quantity < 0 update(product_id, stock_quantity: new_quantity) end end # Usage repo = ProductRepository.new products = repo.active low_stock_items = repo.low_stock(threshold: 5) repo.update_stock(123, -1) # Decrease by 1 ``` -------------------------------- ### Handling InvalidOrderError in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/12_errors.md This Ruby example illustrates how an unknown sort scope call might raise NoMethodError or BetterModel::Errors::Sortable::InvalidOrderError. ```ruby Article.unknown_field_asc ``` -------------------------------- ### Use Meaningful Method Names for Queries Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Illustrates the importance of using descriptive method names that convey business intent rather than generic query identifiers. This enhances code readability and maintainability. ```ruby # Good - describes intent def needs_review search({ status_eq: "draft", created_at_lteq: 24.hours.ago }) end def inactive_users(days: 90) search({ last_sign_in_at_lteq: days.days.ago }) end # Bad - generic names def query1 search({ status_eq: "draft" }) end def get_some_data search({}) end ``` -------------------------------- ### Handling InvalidPredicateError in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/12_errors.md This Ruby example shows how an invalid predicate scope call might raise NoMethodError or BetterModel::Errors::Predicable::InvalidPredicateError. ```ruby Article.title_unknown_predicate("Rails") ``` -------------------------------- ### Implement Search and Filter Methods in Ruby Source: https://github.com/alessiobussolari/better_model/blob/main/context7/11_repositable.md Shows how to implement text search and filtering capabilities within a repository. This includes using 'OR' conditions for multi-field searches and range queries for numerical filters like price. ```ruby class ArticleRepository < ApplicationRepository def model_class = Article def search_articles(query) search({ or: [ { title_i_cont: query }, { content_i_cont: query } ], status_eq: "published" }) end def price_range(min, max) search({ price_between: [min, max] }) end end # Usage repo = ArticleRepository.new results = repo.search_articles("rails tutorial") affordable = repo.price_range(10, 50) ``` -------------------------------- ### Generate Models and Run Migrations Source: https://github.com/alessiobussolari/better_model/blob/main/README.md This sequence of commands first generates traceable, archivable, and stateable models for 'Article' and then applies the pending database migrations. It represents a typical workflow for setting up these features. ```bash # 2. Generate for real rails g better_model:traceable Article --create-table rails g better_model:archivable Article --create-columns rails g better_model:stateable Article --create-tables # 3. Run migrations rails db:migrate ``` -------------------------------- ### Minitest Model Testing for Order Cancellability Source: https://github.com/alessiobussolari/better_model/blob/main/context7/01_statusable.md Shows how to test the `is_cancellable?` method for an Order model using Minitest. This example verifies that an order is cancellable only when paid and not yet shipped. ```ruby class OrderTest < ActiveSupport::TestCase test "cancellable when paid but not shipped" do order = orders(:paid_order) order.update!(shipped_at: nil) assert order.is_cancellable? end test "not cancellable when already shipped" do order = orders(:shipped_order) refute order.is_cancellable? end end ```