{"id":31696627,"url":"https://github.com/kanutocd/whodunit","last_synced_at":"2025-10-08T17:10:00.061Z","repository":{"id":305348496,"uuid":"1022561768","full_name":"kanutocd/whodunit","owner":"kanutocd","description":"Lightweight creator/updater/deleter tracking for ActiveRecord models","archived":false,"fork":false,"pushed_at":"2025-07-27T11:10:01.000Z","size":254,"stargazers_count":7,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-04T21:53:06.892Z","etag":null,"topics":["audit-trail","creator","deleter","lightweight","rails","rubygem","updater"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kanutocd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-19T10:42:53.000Z","updated_at":"2025-07-30T08:39:11.000Z","dependencies_parsed_at":"2025-07-19T18:44:53.045Z","dependency_job_id":null,"html_url":"https://github.com/kanutocd/whodunit","commit_stats":null,"previous_names":["kanutocd/whodunit"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/kanutocd/whodunit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fwhodunit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fwhodunit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fwhodunit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fwhodunit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kanutocd","download_url":"https://codeload.github.com/kanutocd/whodunit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fwhodunit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278898378,"owners_count":26064983,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audit-trail","creator","deleter","lightweight","rails","rubygem","updater"],"created_at":"2025-10-08T17:09:57.446Z","updated_at":"2025-10-08T17:10:00.054Z","avatar_url":"https://github.com/kanutocd.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Whodunit\n\n[![Gem Version](https://badge.fury.io/rb/whodunit.svg)](https://badge.fury.io/rb/whodunit)\n[![CI](https://github.com/kanutocd/whodunit/workflows/CI/badge.svg)](https://github.com/kanutocd/whodunit/actions)\n[![Coverage Status](https://codecov.io/gh/kanutocd/whodunit/branch/main/graph/badge.svg)](https://codecov.io/gh/kanutocd/whodunit)\n[![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.1.0-ruby.svg)](https://www.ruby-lang.org/en/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nLightweight creator/updater/deleter tracking for Rails ActiveRecord models.\n\n\u003e **Fun Fact**: The term \"whodunit\" was coined by literary critic Donald Gordon in 1930 when reviewing a murder mystery novel for _American News of Books_. He described Milward Kennedy's _Half Mast Murder_ as \"a satisfactory whodunit\" - the first recorded use of this now-famous term for mystery stories! _([Source: Wikipedia](https://en.wikipedia.org/wiki/Whodunit))_\n\n## Overview\n\nWhodunit provides simple auditing for Rails applications by tracking who created, updated, and deleted records. Unlike heavyweight solutions like PaperTrail or Audited, Whodunit focuses solely on user tracking with zero performance overhead.\n\n## Requirements\n\n- Ruby 3.1.1+ (tested on 3.1.1, 3.2.0, 3.3.0, 3.4). See the [the ruby-version matrix strategy of the CI workflow](https://github.com/kanutocd/whodunit/blob/main/.github/workflows/ci.yml#L15).\n- Rails 7.2+ (tested on 7.2, 8.2, and edge)\n- ActiveRecord for database operations\n\n## Features\n\n- **Lightweight**: Only tracks user IDs, no change history or versioning\n- **Smart Soft-Delete Detection**: Automatically detects Discard, Paranoia, and custom soft-delete implementations\n- **Thread-Safe**: Uses Rails `CurrentAttributes` pattern for user context\n- **Zero Dependencies**: Only requires Rails 7.2+\n- **Performance Focused**: No default scopes or method overrides\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'whodunit'\n```\n\nAnd then execute:\n\n    $ bundle install\n\n### What's Next?\n\nAfter installation, you have a few options:\n\n1. **Generate Configuration \u0026 Setup** (Recommended):\n\n   ```bash\n   whodunit install\n   ```\n\n   This will:\n\n   - Create `config/initializers/whodunit.rb` with all available configuration options\n   - Optionally add `Whodunit::Stampable` to your `ApplicationRecord` for automatic stamping on all models\n   - Provide clear next steps for adding stamp columns to your database\n\n2. **Quick Setup**: Jump directly to adding stamp columns to your models (see Quick Start below)\n\n3. **Learn More**: Check the [Complete Documentation](https://kanutocd.github.io/whodunit) for advanced configuration\n\n## Quick Start\n\n### 1. Add Stamp Columns\n\nGenerate a migration to add the tracking columns:\n\n```ruby\nclass AddStampsToUsers \u003c ActiveRecord::Migration[7.0]\n  def change\n    add_whodunit_stamps :users  # Adds creator_id, updater_id columns\n  end\nend\n```\n\nFor models with soft-delete, deleter tracking is automatically detected:\n\n```ruby\nclass AddStampsToDocuments \u003c ActiveRecord::Migration[7.0]\n  def change\n    add_whodunit_stamps :documents  # Adds creator_id, updater_id, deleter_id (if soft-delete detected)\n  end\nend\n```\n\n### 2. Include Stampable in Models\n\n```ruby\nclass User \u003c ApplicationRecord\n  include Whodunit::Stampable\nend\n\nclass Document \u003c ApplicationRecord\n  include Discard::Model  # or acts_as_paranoid, etc.\n  include Whodunit::Stampable  # Automatically detects soft-delete!\nend\n```\n\n### 3. Set Up Controller Integration\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  # Whodunit::ControllerMethods is automatically included via Railtie\n  # It will automatically set the current user for stamping\nend\n```\n\n## Usage\n\nOnce set up, stamping happens automatically:\n\n```ruby\n# Creating records\nuser = User.create!(name: \"Ken\")\n# =\u003e Sets user.creator_id to current_user.id\n\n# Updating records\nuser.update!(name: \"Sophia\")\n# =\u003e Sets user.updater_id to current_user.id\n\n# Soft deleting (if soft-delete gem is detected)\ndocument.discard\n# =\u003e Sets document.deleter_id to current_user.id\n```\n\nAccess the stamp information via associations:\n\n```ruby\nuser.creator   # =\u003e User who created this record\nuser.updater   # =\u003e User who last updated this record\nuser.deleter   # =\u003e User who deleted this record (if soft-delete enabled)\n```\n\n## Reverse Associations (Automatic)\n\nWhodunit automatically sets up reverse associations on your User model when you include `Whodunit::Stampable` in other models:\n\n```ruby\n# When you include Whodunit::Stampable in Post model:\nclass Post \u003c ApplicationRecord\n  include Whodunit::Stampable\nend\n\n# Your User model automatically gets these associations:\nuser.created_posts   # =\u003e Posts created by this user\nuser.updated_posts   # =\u003e Posts last updated by this user  \nuser.deleted_posts   # =\u003e Posts deleted by this user (if soft-delete enabled)\n```\n\n### Configuring Reverse Associations\n\n```ruby\n# config/initializers/whodunit.rb\nWhodunit.configure do |config|\n  # Disable automatic reverse associations globally\n  config.auto_setup_reverse_associations = false\n  \n  # Customize association names with prefix/suffix\n  config.reverse_association_prefix = \"whodunit_\"\n  config.reverse_association_suffix = \"_tracked\"\n  # Results in: user.whodunit_created_posts_tracked\nend\n```\n\n### Per-Model Control\n\n```ruby\nclass Post \u003c ApplicationRecord\n  include Whodunit::Stampable\n  \n  # Disable reverse associations for this model only\n  disable_whodunit_reverse_associations!\nend\n\n# Or manually set up later\nPost.setup_whodunit_reverse_associations!\n```\n\n## Soft-Delete Support\n\nWhodunit automatically tracks who deleted records when using soft-delete. Simply configure your soft-delete column:\n\n```ruby\n# Most common soft-delete column (default)\nconfig.soft_delete_column = :deleted_at\n\n# For Discard gem users\nconfig.soft_delete_column = :discarded_at\n\n# For custom implementations\nconfig.soft_delete_column = :archived_at\n\n# Disable soft-delete support\nconfig.soft_delete_column = nil\n```\n\nWhen configured, Whodunit will automatically add the `deleter_id` column to migrations when the soft-delete column is detected in your table.\n\n## Configuration\n\n```ruby\n# config/initializers/whodunit.rb\nWhodunit.configure do |config|\n  config.user_class = 'Account'             # Default: 'User'\n  config.creator_column = :created_by_id    # Default: :creator_id\n  config.updater_column = :updated_by_id    # Default: :updater_id\n  config.deleter_column = :deleted_by_id    # Default: :deleter_id\n  config.soft_delete_column = :discarded_at # Default: nil\n  config.auto_inject_whodunit_stamps = false # Default: true\n\n  # Reverse association configuration\n  config.auto_setup_reverse_associations = false # Default: true\n  config.reverse_association_prefix = \"track_\"   # Default: \"\"\n  config.reverse_association_suffix = \"_logs\"    # Default: \"\"\n\n  # Column data type configuration\n  config.column_data_type = :integer       # Default: :bigint (applies to all columns)\n  config.creator_column_type = :string     # Default: nil (uses column_data_type)\n  config.updater_column_type = :uuid       # Default: nil (uses column_data_type)\n  config.deleter_column_type = :integer    # Default: nil (uses column_data_type)\nend\n```\n\n### Data Type Configuration\n\nBy default, all stamp columns use `:bigint` data type. You can customize this in several ways:\n\n- **Global**: Set `column_data_type` to change the default for all columns\n- **Individual**: Set specific column types to override the global default\n- **Per-migration**: Override types on a per-migration basis (see Migration Helpers)\n\n### Automatic Injection (Rails Integration)\n\nBy default, Whodunit automatically adds stamp columns to your migrations, just like how Rails automatically handles `timestamps`:\n\n```ruby\n# Automatic injection is enabled by default!\n# Your migrations automatically get whodunit stamps:\nclass CreatePosts \u003c ActiveRecord::Migration[8.0]\n  def change\n    create_table :posts do |t|\n      t.string :title\n      t.text :body\n      t.timestamps\n      # t.whodunit_stamps automatically added after t.timestamps!\n    end\n  end\nend\n\n# Disable automatic injection globally:\nWhodunit.configure do |config|\n  config.auto_inject_whodunit_stamps = false\nend\n\n# Skip auto-injection for specific tables:\ncreate_table :system_logs do |t|\n  t.string :message\n  t.timestamps skip_whodunit_stamps: true\nend\n\n# Or add manually if you want specific options:\ncreate_table :posts do |t|\n  t.string :title\n  t.whodunit_stamps include_deleter: true  # Manual override\n  t.timestamps\n  # No auto-injection since already added manually\nend\n```\n\nThis feature respects soft-delete auto-detection and includes the deleter column when appropriate.\n\n## Manual User Setting\n\nFor background jobs, tests, or special scenarios:\n\n```ruby\n# Temporarily set user\nWhodunit::Current.user = User.find(123)\nMyModel.create!(name: \"test\")  # Will be stamped with user 123\n\n# Within a block\ncontroller.with_whodunit_user(admin_user) do\n  Document.create!(title: \"Admin Document\")\nend\n\n# Disable stamping temporarily\ncontroller.without_whodunit_user do\n  Document.create!(title: \"System Document\")  # No stamps\nend\n```\n\n## Migration Helpers\n\n```ruby\n# Basic usage (uses configured data types)\nclass CreatePosts \u003c ActiveRecord::Migration[7.0]\n  def change\n    create_table :posts do |t|\n      t.string :title\n      t.whodunit_stamps  # Adds creator_id, updater_id with configured types\n      t.timestamps\n    end\n  end\nend\n\n# Custom data types per migration\nclass CreateUsers \u003c ActiveRecord::Migration[7.0]\n  def change\n    create_table :users do |t|\n      t.string :email\n      t.whodunit_stamps include_deleter: true,\n                        creator_type: :uuid,\n                        updater_type: :string,\n                        deleter_type: :integer\n      t.timestamps\n    end\n  end\nend\n\n# Add to existing table with custom types\nclass AddStampsToExistingTable \u003c ActiveRecord::Migration[7.0]\n  def change\n    add_whodunit_stamps :existing_table,\n                        include_deleter: :auto,\n                        creator_type: :string,\n                        updater_type: :uuid\n  end\nend\n\n# Mixed approach - some custom, some default\nclass CreateDocuments \u003c ActiveRecord::Migration[7.0]\n  def change\n    create_table :documents do |t|\n      t.string :title\n      t.whodunit_stamps creator_type: :uuid  # Only override creator, others use defaults\n      t.timestamps\n    end\n  end\nend\n```\n\n### Data Type Options\n\nCommon data types you can use:\n\n- `:bigint` (default) - 64-bit integer, suitable for large user bases\n- `:integer` - 32-bit integer, suitable for smaller applications\n- `:string` - For string-based user identifiers\n- `:uuid` - For UUID-based user systems\n- Any other Rails column type\n\n## Controller Methods\n\nSkip stamping for specific actions:\n\n```ruby\nclass ApiController \u003c ApplicationController\n  skip_whodunit_for :index, :show\nend\n```\n\nOnly stamp specific actions:\n\n```ruby\nclass ReadOnlyController \u003c ApplicationController\n  whodunit_only_for :create, :update, :destroy\nend\n```\n\n## Thread Safety\n\nWhodunit uses Rails `CurrentAttributes` for thread-safe user context:\n\n```ruby\n# Each thread maintains its own user context\nThread.new { Whodunit::Current.user = user1; create_records }\nThread.new { Whodunit::Current.user = user2; create_records }\n```\n\n## Testing\n\nIn your tests, you can set the user context:\n\n```ruby\n# RSpec\nbefore do\n  Whodunit::Current.user = create(:user)\nend\n\n# Or within specific tests\nit \"tracks creator\" do\n  user = create(:user)\n  Whodunit::Current.user = user\n\n  post = create(:post)\n  expect(post.creator).to eq(user)\nend\n```\n\n## Comparisons\n\n| Feature               | Whodunit | PaperTrail | Audited |\n| --------------------- | -------- | ---------- | ------- |\n| User tracking         | ✅       | ✅         | ✅      |\n| Change history        | Via [whodunit-chronicles](https://github.com/kanutocd/whodunit-chronicles) | ✅         | ✅      |\n| Performance overhead  | None     | High       | Medium  |\n| Soft-delete support   | ✅       | ❌         | ❌      |\n| Setup complexity      | Low      | Medium     | Medium  |\n\n## Documentation\n\nComplete API documentation is available at: **[https://kanutocd.github.io/whodunit](https://kanutocd.github.io/whodunit)**\n\nThe documentation includes:\n\n- Comprehensive API reference with examples\n- Configuration options and their defaults\n- Migration helper methods\n- Controller integration patterns\n- Advanced usage scenarios\n\nTo generate documentation locally:\n\n```bash\nbundle exec yard doc\nopen doc/index.html\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec gem build whodunit.gemspec \u0026\u0026 gem install ./whodunit-*.gem`.\n\n### Testing\n\n```bash\n# Run all tests\nbundle exec rspec\n\n# Run tests with coverage\nCOVERAGE=true bundle exec rspec\n\n# Run RuboCop\nbundle exec rubocop\n\n# Run security audit\nbundle exec bundle audit check --update\n\n# Generate documentation\nbundle exec yard doc\n```\n\n### Release Process\n\nThe gem uses automated CI/CD workflows:\n\n- **CI**: Automatically runs tests, linting, and security checks on every push and PR\n- **Release**: Supports both automatic releases (on GitHub release creation) and manual releases via workflow dispatch\n- **Documentation**: Automatically deploys API documentation to GitHub Pages\n\nTo perform a release:\n\n1. **Dry Run**: Test the release process without publishing\n\n   ```bash\n   # Via GitHub Actions UI: Run \"Release\" workflow with dry_run=true\n   ```\n\n2. **Create Release**:\n\n   ```bash\n   # Update version in lib/whodunit/version.rb\n   # Commit and push changes\n   # Create a GitHub release via UI or CLI\n   gh release create v0.1.0 --title \"Release v0.1.0\" --notes \"Release notes here\"\n   ```\n\n3. **Manual Release** (if needed):\n   ```bash\n   # Via GitHub Actions UI: Run \"Release\" workflow with dry_run=false\n   ```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/kanutocd/whodunit.\n\n### Development Workflow\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Add tests for new functionality\n5. Ensure all tests pass (`bundle exec rspec`)\n6. Run RuboCop and fix any style issues (`bundle exec rubocop`)\n7. Update documentation if needed\n8. Commit your changes (`git commit -am 'Add amazing feature'`)\n9. Push to the branch (`git push origin feature/amazing-feature`)\n10. Open a Pull Request\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanutocd%2Fwhodunit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkanutocd%2Fwhodunit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanutocd%2Fwhodunit/lists"}