active_capture 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c6e10fd5af7be677b0441dd76aaa567955953ba576753b10e3130daaff2b01d0
4
+ data.tar.gz: 4506c04f93aefb84f841dc9c591ccebe5dd24c3199f9056e9b39999cf4184c2e
5
+ SHA512:
6
+ metadata.gz: f3ce3ab62a22da2e1c0e34cfc1ed13081f8a477facdfff0d3d13cb1f6aaa140981300d67f108afc1ccb99875edc0443aaf45569c0c3bb5eb71c6068339683dd0
7
+ data.tar.gz: 79e418aab36f10f116120753cc4962e2a9933d22649292a424afc6efc89945ee5f95230701c3d3d1ebc534914523af3bd4e787bf0052eb6fd81ef6b3cf4d81db
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ module ActiveCapture
5
+ class SnapshotStorage
6
+ SNAPSHOT_DIR = "snapshots"
7
+
8
+ def self.save_snapshot(record, snapshot_data, name)
9
+ model_name = record.class.name.downcase
10
+ record_id = record.id
11
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
12
+ file_name = "#{record_id}_#{timestamp}.json"
13
+
14
+ dir_path = File.join(SNAPSHOT_DIR, model_name)
15
+ FileUtils.mkdir_p(dir_path)
16
+
17
+ file_path = File.join(dir_path, file_name)
18
+ File.write(file_path, JSON.pretty_generate(snapshot_data))
19
+
20
+ puts "Snapshot saved to #{file_path}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCapture
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,88 @@
1
+ require "active_capture/version"
2
+ require_relative "active_capture/snapshot_storage"
3
+
4
+ module ActiveCapture
5
+ class Snapshot
6
+ def self.take(record, include_associations: [], name: nil)
7
+ raise ArgumentError, "Record must be an ActiveRecord::Base instance" unless record.is_a?(ActiveRecord::Base)
8
+
9
+ snapshot_data = {
10
+ model: record.class.name,
11
+ record_id: record.id,
12
+ attributes: record.attributes,
13
+ associations: capture_associations(record, include_associations)
14
+ }
15
+
16
+ SnapshotStorage.save_snapshot(record, snapshot_data, name)
17
+ end
18
+
19
+ def self.restore(record, snapshot_file)
20
+ snapshot_data = JSON.parse(File.read(snapshot_file))
21
+
22
+ raise ArgumentError, "Snapshot file does not match the given record" unless snapshot_data["record_id"] == record.id
23
+
24
+ # Restore record attributes
25
+ record.update!(snapshot_data["attributes"])
26
+
27
+ # Restore associations
28
+ restore_associations(record, snapshot_data["associations"])
29
+ end
30
+
31
+ private
32
+
33
+ def self.capture_associations(record, associations)
34
+ association_data = {}
35
+
36
+ associations.each do |association|
37
+ nested_associations = []
38
+
39
+ if association.is_a?(Hash)
40
+ association_name = association.keys.first
41
+ nested_associations = association.values.flatten
42
+ else
43
+ association_name = association
44
+ end
45
+
46
+ if record.respond_to?(association_name)
47
+ related_records = record.send(association_name)
48
+
49
+ if related_records.is_a?(ActiveRecord::Base)
50
+ association_data[association_name] = {
51
+ attributes: related_records.attributes,
52
+ associations: capture_associations(related_records, nested_associations)
53
+ }
54
+ elsif related_records.respond_to?(:map)
55
+ association_data[association_name] = related_records.map do |related_record|
56
+ {
57
+ attributes: related_record.attributes,
58
+ associations: capture_associations(related_record, nested_associations)
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ association_data
66
+ end
67
+
68
+ def self.restore_associations(record, associations_data)
69
+ associations_data.each do |association_name, related_data|
70
+ if related_data.is_a?(Array)
71
+ # Restore has_many associations
72
+ associated_class = record.class.reflect_on_association(association_name).klass
73
+ record.send(association_name).destroy_all
74
+
75
+ related_data.each do |related_record_data|
76
+ associated_record = associated_class.create!(related_record_data["attributes"])
77
+ restore_associations(associated_record, related_record_data["associations"])
78
+ end
79
+ else
80
+ # Restore belongs_to or has_one association
81
+ associated_class = record.class.reflect_on_association(association_name).klass
82
+ associated_record = associated_class.create!(related_data["attributes"])
83
+ restore_associations(associated_record, related_data["associations"])
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,44 @@
1
+ require "test_helper"
2
+ require "minitest/autorun"
3
+
4
+ class ActiveCaptureTest < Minitest::Test
5
+ def setup
6
+ @user = User.create!(name: "John Doe", email: "john@example.com")
7
+ @post = @user.posts.create!(title: "First Post", content: "This is my first post.")
8
+ @comment1 = @post.comments.create!(content: "Great post!")
9
+ @comment2 = @post.comments.create!(content: "Thanks for sharing.")
10
+ end
11
+
12
+ def test_snapshot_with_nested_associations
13
+ ActiveCapture::Snapshot.take(@user, include_associations: [{ posts: :comments }])
14
+
15
+ snapshot_files = Dir["snapshots/user/*.json"]
16
+ assert !snapshot_files.empty?, "Snapshot file should be created"
17
+
18
+ snapshot_content = JSON.parse(File.read(snapshot_files.last))
19
+ assert_equal 1, snapshot_content["associations"]["posts"].size
20
+ assert_equal 2, snapshot_content["associations"]["posts"].first["associations"]["comments"].size
21
+ end
22
+
23
+ def test_restore_from_snapshot
24
+ ActiveCapture::Snapshot.take(@user, include_associations: [{ posts: :comments }])
25
+ snapshot_file = Dir["snapshots/user/*.json"].last
26
+
27
+ # Modify records
28
+ @user.update!(name: "Jane Doe")
29
+ @post.update!(title: "Updated Post")
30
+ @comment1.destroy
31
+
32
+ # Restore from snapshot
33
+ ActiveCapture::Snapshot.restore(@user, snapshot_file)
34
+
35
+ # Reload records
36
+ @user.reload
37
+ @post.reload
38
+
39
+ # Assertions
40
+ assert_equal "John Doe", @user.name
41
+ assert_equal "First Post", @post.title
42
+ assert_equal 2, @post.comments.count
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ # test/test_helper.rb
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__) # This line makes sure your gem's lib directory is included in the load path.
3
+ require "active_capture"
4
+ require "minitest/autorun"
5
+ require "active_record"
6
+
7
+ # Setup an in-memory SQLite database for testing
8
+ ActiveRecord::Base.establish_connection(
9
+ adapter: "sqlite3",
10
+ database: ":memory:"
11
+ )
12
+
13
+ ActiveRecord::Schema.define do
14
+ create_table :users, force: true do |t|
15
+ t.string :name
16
+ t.string :email
17
+ t.timestamps
18
+ end
19
+
20
+ create_table :posts, force: true do |t|
21
+ t.integer :user_id
22
+ t.string :title
23
+ t.text :content
24
+ t.timestamps
25
+ end
26
+
27
+ create_table :comments, force: true do |t|
28
+ t.integer :post_id
29
+ t.string :content
30
+ t.timestamps
31
+ end
32
+ end
33
+
34
+ class User < ActiveRecord::Base
35
+ has_many :posts
36
+ end
37
+
38
+ class Post < ActiveRecord::Base
39
+ belongs_to :user
40
+ has_many :comments
41
+ end
42
+
43
+ class Comment < ActiveRecord::Base
44
+ belongs_to :post
45
+ end
46
+
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_capture
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Tanmay Bhawsar
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-03-29 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ description: ActiveCapture allows you to save snapshots of ActiveRecord records, including
27
+ their nested associations, and restore them at any point. Useful for auditing, backups,
28
+ and rollback functionality.
29
+ email: bhawsartanmay@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/active_capture.rb
35
+ - lib/active_capture/snapshot_storage.rb
36
+ - lib/active_capture/version.rb
37
+ - test/test_active_capture.rb
38
+ - test/test_helper.rb
39
+ homepage: https://github.com/bhawsartanmay/active_capture
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ homepage_uri: https://github.com/bhawsartanmay/active_capture
44
+ source_code_uri: https://github.com/bhawsartanmay/active_capture
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.1.0
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.6.2
60
+ specification_version: 4
61
+ summary: A Ruby on Rails gem for taking and restoring snapshots of ActiveRecord records
62
+ with nested associations.
63
+ test_files: []