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 +7 -0
- data/lib/active_capture/snapshot_storage.rb +23 -0
- data/lib/active_capture/version.rb +5 -0
- data/lib/active_capture.rb +88 -0
- data/test/test_active_capture.rb +44 -0
- data/test/test_helper.rb +46 -0
- metadata +63 -0
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,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
|
data/test/test_helper.rb
ADDED
@@ -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: []
|