ActiveSnapshot

repository·master·Indexed 41 days ago

https://github.com/westonganger/active_snapshot

A Ruby gem providing simplified, transparent snapshotting and restoration for ActiveRecord models and their associations. It allows developers to create snapshots of records and child associations using `has_snapshot_children`, restore them via `restore!`, and calculate differences between states using `ActiveSnapshot::Snapshot.diff`. It supports multiple storage methods including `native_json`, `serialized_json`, and `serialized_yaml`.

Tokens
4.8K
Snippets
19
Records
20
Agent score
52%

What's inside ActiveSnapshot

  1. Track associated or child records with `has_snapshot_children`

    master

    To ensure that child records (associations) are included in a snapshot, use the has_snapshot_children block in your model. The block is executed in the context of the instance (self).

    Inside the block, you should:

    1. Reload the record from the database to ensure a clean state and eager load associations.
    2. Return a hash mapping association names to the records or specific configurations to be restored.

    Supported configurations in the hash:

    • Direct records: Mapping a key to an association (e.g., comments: instance.comments).
    • Nested associations: Mapping to an array of records (e.g., comment_sub_records: instance.comments.flat_map{|x| x.comment_sub_records }).
    • Custom record mapping: Providing a hash with :records (e.g., tags: { records: instance.tags }).
    • Custom deletion logic: Providing a hash with :record and a :delete_method proc (e.g., ip_address: { record: instance.ip_address, delete_method: ->(item){ item.release! } }).
    class Post < ActiveRecord::Base
      include ActiveSnapshot
    
      has_snapshot_children do
        # Reload to ensure clean state and eager loading
        instance = self.class.includes(:tags, :ip_address, comments: [:comment_sub_records]).find(id)
    
        {
          comments: instance.comments,
          comment_sub_records: instance.comments.flat_map{|x| x.comment_sub_records },
          tags: {
            records: instance.tags
          },
          ip_address: {
            record: instance.ip_address,
            delete_method: ->(item){ item.release! }
          }
        }
      end
    end
  2. Install ActiveSnapshot

    master

    Add the gem to your Gemfile:

    gem 'active_snapshot'

    Then, run the installation generator and migrate your database to create the required snapshots and snapshot_items tables:

    rails generate active_snapshot:install
    rake db:migrate

    Finally, include the module in your ApplicationRecord or specific models to enable snapshotting capabilities.

    class ApplicationRecord < ActiveRecord::Base
      include ActiveSnapshot
    end
  3. Warning: Dropping database columns

    master

    Dropping a database column will break the restore mechanism for existing snapshots, as the application will be unable to assign data to non-existent columns. ActiveSnapshot attempts to work around this by bypassing attribute assignment for keys that do not correlate to current database columns, but this should be handled carefully in your migration guidelines.

    To detect if existing snapshots contain data for dropped columns, you can run this diagnostic script:

    SnapshotItem.all.each do |snapshot_item|
      snapshot_item.object.keys.each do |key|
        klass = Class.const_get(snapshot_item.item_type)
    
        if !klass.column_names.include?(key)
          invalid_data = snapshot_item.object.slice(*klass.column_names)
    
          raise "invalid data found - #{invalid_data}"
        end
      end
    end
  4. Diff two snapshots or a snapshot and a record

    master

    You can calculate the differences between two snapshots or between a snapshot and the current state of a record using ActiveSnapshot::Snapshot.diff.

    # Diff between two snapshots
    from = post.snapshots.first
    to = post.snapshots.second
    diff = ActiveSnapshot::Snapshot.diff(from, to)
    
    # Diff between a snapshot and the current record
    from = post.snapshots.last
    diff = ActiveSnapshot::Snapshot.diff(from, post)
    
    # Example output format:
    # [
    #   {action: :update, item_type: "Post", item_id: 1, changes: {name: ["Old Name", "New Name"]}},
    #   {action: :destroy, item_type: "Comment", item_id: 1, changes: {id: [1, nil], content: ["Some Content", nil]}},
    #   {action: :create, item_type: "Comment", item_id: 2, changes: {id: [nil, 1], content: [nil, "New Content"]}}
    # ]
  5. Reify snapshot items

    master

    Reification allows you to view ActiveRecord instances where the local object data matches the snapshotted data, even if the database has changed.

    By default, reified records are set to readonly. To modify them, you must explicitly set the readonly attribute to false.

    # Fetch reified items (returns [parent_record, children_hash])
    # Children are readonly by default
    reified_parent, reified_children_hash = snapshot.fetch_reified_items
    
    # To make them writable:
    reified_parent, reified_children_hash = snapshot.fetch_reified_items(readonly: false)
    # OR
    reified_children_hash.first.instance_variable_set("@readonly", false)
  6. Create, Restore, and Destroy snapshots

    master

    Once ActiveSnapshot is included in your model, you can manage snapshots using the following instance methods:

    • create_snapshot!(options): Creates a new snapshot. All options are optional. Common options include identifier, user, and metadata.
    • restore!: Restores the record and any associated child records defined via has_snapshot_children.
    • destroy!: Manually destroys the snapshot and all its child snapshots. Note: Snapshots and snapshot items are NEVER destroyed automatically.
    post = Post.first
    
    # Create a snapshot
    snapshot = post.create_snapshot!(
      identifier: "snapshot_1",
      user: current_user,
      metadata: { foo: :bar }
    )
    
    # Restore the snapshot
    snapshot.restore!
    
    # Destroy the snapshot manually
    snapshot.destroy!
  7. Configure the storage_method for ActiveSnapshot

    master

    The storage_method setting determines how snapshot data is persisted. You can explicitly set this value to one of the supported formats. If not configured, the library attempts to detect the appropriate method based on the database schema (defaulting to serialized_json if the object attribute is a text type, otherwise native_json).

    Supported values:

    • serialized_yaml
    • serialized_json
    • native_json

    Setting an invalid value will raise an ArgumentError.

    # Example of setting the storage method
    ActiveSnapshot.configure do |config|
      config.storage_method = 'native_json'
    end
  8. Restore a snapshot using restore!

    master

    The restore! method reverts the associated resource and its children to the state captured in the snapshot. This operation is performed within an ActiveRecord::Base.transaction.

    Behavior:

    1. Cleanup: It identifies child records that were present in the resource but are not included in the snapshot's snapshot_items. It then removes them using the delete_method defined in the resource's children_to_snapshot mapping (defaults to destroy!).
    2. Restoration: It iterates through all snapshot_items and calls restore_item! on each to recreate or update the records to their snapshotted state.

    Warning: Snapshots and snapshot items are never destroyed automatically; you must manage their lifecycle manually.

    snapshot = ActiveSnapshot::Snapshot.find(id)
    snapshot.restore!
  9. Retrieve reified items from a snapshot using fetch_reified_items

    master

    If you want to inspect the data captured in a snapshot without actually restoring it to the database, use fetch_reified_items. This method reconstructs the objects in memory.

    Parameters:

    • readonly: (Boolean, default: true) If true, the returned objects are marked as readonly! to prevent accidental database writes.

    Return Value: Returns an array containing two elements: [reified_parent, reified_children_hash].

    • reified_parent: The reconstructed instance of the main resource.
    • reified_children_hash: A hash where keys are the child_group_name and values are arrays of the reconstructed child objects.
    parent, children = snapshot.fetch_reified_items(readonly: true)
    
    # Accessing data:
    puts parent.name
    puts children[:comments].first.body