{"id":13878616,"url":"https://github.com/westonganger/active_snapshot","last_synced_at":"2025-12-28T21:52:14.026Z","repository":{"id":43385856,"uuid":"299555247","full_name":"westonganger/active_snapshot","owner":"westonganger","description":"Simplified snapshots and restoration for ActiveRecord models and associations with a transparent white-box implementation","archived":false,"fork":false,"pushed_at":"2025-01-18T00:47:11.000Z","size":180,"stargazers_count":163,"open_issues_count":3,"forks_count":19,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-07T20:09:45.518Z","etag":null,"topics":["activerecord","associations","audit","model-snapshots","model-versioning","rails","restore-data","ruby","versioning"],"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/westonganger.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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}},"created_at":"2020-09-29T08:37:10.000Z","updated_at":"2025-03-06T20:10:15.000Z","dependencies_parsed_at":"2023-02-18T17:15:58.210Z","dependency_job_id":"d8b63e16-61c8-4afb-823b-388753001591","html_url":"https://github.com/westonganger/active_snapshot","commit_stats":{"total_commits":86,"total_committers":8,"mean_commits":10.75,"dds":"0.12790697674418605","last_synced_commit":"67308c82719df1fc5932c8ea7f6b05903273beb9"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/westonganger%2Factive_snapshot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/westonganger%2Factive_snapshot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/westonganger%2Factive_snapshot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/westonganger%2Factive_snapshot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/westonganger","download_url":"https://codeload.github.com/westonganger/active_snapshot/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254364270,"owners_count":22058878,"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","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":["activerecord","associations","audit","model-snapshots","model-versioning","rails","restore-data","ruby","versioning"],"created_at":"2024-08-06T08:01:54.855Z","updated_at":"2025-12-28T21:52:14.021Z","avatar_url":"https://github.com/westonganger.png","language":"Ruby","funding_links":[],"categories":["Ruby","ORM/ODM Extensions"],"sub_categories":[],"readme":"# ActiveSnapshot\n\n\u003ca href=\"https://badge.fury.io/rb/active_snapshot\" target=\"_blank\"\u003e\u003cimg height=\"21\" style='border:0px;height:21px;' border='0' src=\"https://badge.fury.io/rb/active_snapshot.svg\" alt=\"Gem Version\"\u003e\u003c/a\u003e\n\u003ca href='https://github.com/westonganger/active_snapshot/actions' target='_blank'\u003e\u003cimg src=\"https://github.com/westonganger/active_snapshot/actions/workflows/test.yml/badge.svg?branch=master\" style=\"max-width:100%;\" height='21' style='border:0px;height:21px;' border='0' alt=\"CI Status\"\u003e\u003c/a\u003e\n\u003ca href='https://rubygems.org/gems/active_snapshot' target='_blank'\u003e\u003cimg height='21' style='border:0px;height:21px;' src='https://img.shields.io/gem/dt/active_snapshot?color=brightgreen\u0026label=Rubygems%20Downloads' border='0' alt='RubyGems Downloads' /\u003e\u003c/a\u003e\n\nSimplified snapshots and restoration for ActiveRecord models and associations with a transparent white-box implementation.\n\nKey Features:\n\n- Create and Restore snapshots of a parent record and any specified child records\n- Predictable and explicit behaviour provides much needed clarity to your restore logic\n- Snapshots are created upon request only, we do not use any callbacks\n- Tiny method footprint so its easy to completely override the logic later\n\nWhy This Library:\n\nModel Versioning and Restoration require conscious thought, design, and understanding. You should understand your versioning and restoration process completely. This gem's small API and fully understandable design fully supports this.\n\nI do not recommend using [paper_trail-association_tracking](https://github.com/westonganger/paper_trail-association_tracking) because it is mostly a blackbox solution which encourages you to set it up and then assume its Just Working\u003csup\u003eTM\u003c/sup\u003e. This makes for major data problems later. Dont fall into this trap. Instead read this gems brief source code completely before use OR copy the code straight into your codebase. Once you know it, then you are free.\n\n\n\n# Installation\n\n```ruby\ngem 'active_snapshot'\n```\n\nThen generate and run the necessary migrations to setup the `snapshots` and `snapshot_items` tables.\n\n```\nrails generate active_snapshot:install\nrake db:migrate\n```\n\nThen add `include ActiveSnapshot` to your ApplicationRecord or individual models.\n\n```ruby\nclass ApplicationRecord \u003c ActiveRecord::Base\n  include ActiveSnapshot\nend\n```\n\nThis defines the following associations on your models:\n\n```ruby\nhas_many :snapshots, as: :item, class_name: 'Snapshot'\nhas_many :snapshot_items, as: :item, class_name: 'SnapshotItem'\n```\n\nIt defines an optional extension to your model: `has_snapshot_children`.\n\nIt defines one instance method to your model: `create_snapshot!`\n\n# Basic Usage\n\nYou now have access to the following methods:\n\n```ruby\npost = Post.first\n\n# Create snapshot, all fields are optional\nsnapshot = post.create_snapshot!(\n  identifier: \"snapshot_1\",\n  user: current_user,\n  metadata: {\n    foo: :bar\n  },\n)\n\n# Restore snapshot and all its child snapshots\nsnapshot.restore!\n\n# Destroy snapshot and all its child snapshots\n# must be performed manually, snapshots and snapshot items are NEVER destroyed automatically\nsnapshot.destroy!\n```\n\n# Tracking Associated / Child Records\n\n```ruby\nclass Post \u003c ActiveRecord::Base\n  include ActiveSnapshot\n\n  has_snapshot_children do\n    ### Executed in the context of the instance / self\n\n    ### Reload record from database to ensure a clean state and eager load the specified associations\n    instance = self.class.includes(:tags, :ip_address, comments: [:comment_sub_records]).find(id)\n\n    ### Define the associated records that will be restored\n    {\n      comments: instance.comments,\n\n      ### Nested Associations can be handled by simply mapping them into an array\n      comment_sub_records: instance.comments.flat_map{|x| x.comment_sub_records },\n\n      tags: {\n        records: instance.tags\n      },\n\n      ip_address: {\n        record: instance.ip_address,\n        delete_method: -\u003e(item){ item.release! }\n      }\n    }\n  end\n\nend\n```\n\nNow when you run `create_snapshot!` the associations will be tracked accordingly\n\n# Reifying Snapshots\n\nA reified record refers to an ActiveRecord instance where the local objects data is set to match the snaphotted data, but the database remains changed.\n\nYou can view all of the \"reified\" snapshot items by calling the following method. Its completely up to you on how to use this data.\n\n```ruby\nreified_parent, reified_children_hash = snapshot.fetch_reified_items\n```\n\nAs a safety these records have the `readonly` attribute set on them.\nIf you want to perform any write actions on the returned instances you will have to set the `readonly` attribute to `false`\n\n```ruby\nreified_parent, reified_children_hash = snapshot.fetch_reified_items(readonly: false)\n# or\nreified_parent, reified_children_hash = snapshot.fetch_reified_items\nreified_children_hash.first.instance_variable_set(\"@readonly\", false)\n```\n\n# Diffing Versions\n\nYou can obtain the diff between two snapshots like this:\n```ruby\nfrom = post.snapshots.first\nto = post.snapshots.second\n\nActiveSnapshot::Snapshot.diff(from, to)\n# [\n#   {action: :update, item_type: \"Post\", item_id: 1, changes: {name: [\"Old Name\", \"New Name\"]}},\n#   {action: :destroy, item_type: \"Comment\", item_id: 1, changes: {id: [1, nil], content: [\"Some Content\", nil]}},\n#   {action: :create, item_type: \"Comment\", item_id: 2, changes: {id: [nil, 1], content: [nil, \"New  Content\"]}}\n# ]\n```\n\nYou can also obtain the diff between a snapshot and the current record:\n```ruby\nfrom = post.snapshots.last\n\nActiveSnapshot::Snapshot.diff(from, post)\n```\n\n# Important Data Considerations / Warnings\n\n### Dropping columns\n\nIf you plan to use the snapshot restore capabilities please be aware:\n\nWhenever you drop a database column and there already exists snapshots of that model then you are kind of silently breaking your restore mechanism. Because now the application will not be able to assign data to columns that dont exist on the model. We work around this by bypassing the attribute assignment for snapshot item object entries that does not correlate to a current database column.\n\nI recommend that you add an entry to this in your applications safe-migrations guidelines.\n\nIf you would like to detect if this situation has already ocurred you can use the following script:\n\n```ruby\nSnapshotItem.all.each do |snapshot_item|\n  snapshot_item.object.keys.each do |key|\n    klass = Class.const_get(snapshot_item.item_type)\n\n    if !klass.column_names.include?(key)\n      invalid_data = snapshot_item.object.slice(*klass.column_names)\n\n      raise \"invalid data found - #{invalid_data}\"\n    end\n  end\nend\n```\n\n# Key Models Provided \u0026 Additional Customizations\n\nA key aspect of this library is its simplicity and small API. For major functionality customizations we encourage you to first delete this gem and then copy this gems code directly into your repository.\n\nI strongly encourage you to read the code for this library to understand how it works within your project so that you are capable of customizing the functionality later.\n\n- [SnapshotsConcern](./lib/active_snapshot/models/concerns/snapshots_concern.rb)\n  * Defines `snapshots` and `snapshot_items` has_many associations\n  * Defines `create_snapshot!` and `has_snapshot_children` methods\n- [Snapshot](./lib/active_snapshot/models/snapshot.rb)\n  * Contains a unique `identifier` column (optional, but available for custom identification purposes)\n  * `has_many :item_snapshots`\n- [SnapshotItem](./lib/active_snapshot/models/snapshot_item.rb)\n  * Contains `object` column which contains an encoded database row\n  * `belongs_to :snapshot`\n\n\n# Credits\n\nCreated \u0026 Maintained by [Weston Ganger](https://westonganger.com) - [@westonganger](https://github.com/westonganger)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwestonganger%2Factive_snapshot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwestonganger%2Factive_snapshot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwestonganger%2Factive_snapshot/lists"}