active_capture 0.1.1 → 1.0.0
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 +4 -4
- data/lib/active_capture/capture_storage.rb +30 -0
- data/lib/active_capture/version.rb +1 -1
- data/lib/active_capture.rb +44 -26
- data/test/test_active_capture.rb +17 -23
- metadata +5 -5
- data/lib/active_capture/snapshot_storage.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 874ca90fb90863834525adb945cfead2334c37dcf115b3768c5c1500ac8cc85f
|
4
|
+
data.tar.gz: 888263dff4492a4b952e1dc491a49d0cda55c31777c7ee1db3de3e0df6823b89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44c1c5a0cbf60145bc0c60d1b0528f2a2f7ea52f4540468ef9630dd3c3a5e2d40fcf1fab1cba86ea1da7b514ea55c14005f4da0c6c379dc00b0909e714b55f13
|
7
|
+
data.tar.gz: aca8f8322c896f41b84ae434cd4505a313e266d1c13f4b7c94896de25dd0025d411a697a93403e4190be580c0d2d63b6957fb0db43f632cb5d4d301a27466ae3
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
module ActiveCapture
|
6
|
+
class CaptureStorage
|
7
|
+
CAPTURE_DIR = 'captures'
|
8
|
+
|
9
|
+
def self.save_capture(record, capture_data, name = nil)
|
10
|
+
model_name = record.class.name.downcase
|
11
|
+
record_id = record.id
|
12
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
13
|
+
file_name = name ? "#{name}.json" : "#{record_id}_#{timestamp}.json"
|
14
|
+
|
15
|
+
dir_path = File.join(CAPTURE_DIR, model_name)
|
16
|
+
FileUtils.mkdir_p(dir_path)
|
17
|
+
|
18
|
+
file_path = File.join(dir_path, file_name)
|
19
|
+
File.write(file_path, JSON.pretty_generate(capture_data))
|
20
|
+
|
21
|
+
puts "Capture saved to #{file_path}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_capture(capture_file)
|
25
|
+
raise ArgumentError, "Capture file does not exist #{capture_file}" unless File.exist?(capture_file)
|
26
|
+
|
27
|
+
JSON.parse(File.read(capture_file))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/active_capture.rb
CHANGED
@@ -1,31 +1,32 @@
|
|
1
|
-
require
|
2
|
-
require_relative
|
1
|
+
require 'active_capture/version'
|
2
|
+
require_relative 'active_capture/capture_storage'
|
3
|
+
require 'json'
|
4
|
+
require 'active_record'
|
3
5
|
|
4
6
|
module ActiveCapture
|
5
|
-
class
|
6
|
-
def self.take(record,
|
7
|
-
raise ArgumentError,
|
7
|
+
class Capture
|
8
|
+
def self.take(record, associations: [], name: nil)
|
9
|
+
raise ArgumentError, 'Record must be an ActiveRecord::Base instance' unless record.is_a?(ActiveRecord::Base)
|
8
10
|
|
9
|
-
|
11
|
+
capture_data = {
|
10
12
|
model: record.class.name,
|
11
13
|
record_id: record.id,
|
12
14
|
attributes: record.attributes,
|
13
|
-
associations: capture_associations(record,
|
15
|
+
associations: capture_associations(record, associations)
|
14
16
|
}
|
15
17
|
|
16
|
-
|
18
|
+
CaptureStorage.save_capture(record, capture_data, name)
|
17
19
|
end
|
18
20
|
|
19
|
-
def self.restore(record,
|
20
|
-
|
21
|
+
def self.restore(record, capture_file, merge: false)
|
22
|
+
captured_records = CaptureStorage.load_capture(capture_file)
|
21
23
|
|
22
|
-
raise ArgumentError,
|
24
|
+
raise ArgumentError, 'capture file does not match the given record' unless captured_records['record_id'] == record.id
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
restore_associations(record, snapshot_data["associations"])
|
26
|
+
ActiveRecord::Base.transaction do
|
27
|
+
record.update!(captured_records['attributes'])
|
28
|
+
restore_associations(record, captured_records['associations'], merge: merge)
|
29
|
+
end
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
@@ -35,7 +36,7 @@ module ActiveCapture
|
|
35
36
|
|
36
37
|
associations.each do |association|
|
37
38
|
nested_associations = []
|
38
|
-
|
39
|
+
|
39
40
|
if association.is_a?(Hash)
|
40
41
|
association_name = association.keys.first
|
41
42
|
nested_associations = association.values.flatten
|
@@ -45,7 +46,7 @@ module ActiveCapture
|
|
45
46
|
|
46
47
|
if record.respond_to?(association_name)
|
47
48
|
related_records = record.send(association_name)
|
48
|
-
|
49
|
+
|
49
50
|
if related_records.is_a?(ActiveRecord::Base)
|
50
51
|
association_data[association_name] = {
|
51
52
|
attributes: related_records.attributes,
|
@@ -65,22 +66,39 @@ module ActiveCapture
|
|
65
66
|
association_data
|
66
67
|
end
|
67
68
|
|
68
|
-
def self.restore_associations(record, associations_data)
|
69
|
+
def self.restore_associations(record, associations_data, merge: false)
|
69
70
|
associations_data.each do |association_name, related_data|
|
70
71
|
if related_data.is_a?(Array)
|
71
|
-
# Restore has_many associations
|
72
72
|
associated_class = record.class.reflect_on_association(association_name).klass
|
73
|
-
record.send(association_name)
|
73
|
+
existing_records = record.send(association_name)
|
74
74
|
|
75
75
|
related_data.each do |related_record_data|
|
76
|
-
|
77
|
-
|
76
|
+
if related_record_data['attributes']['id'] && existing_record = existing_records.find_by(id: related_record_data['attributes']['id'])
|
77
|
+
existing_record.update!(related_record_data['attributes'])
|
78
|
+
restore_associations(existing_record, related_record_data['associations'], merge: merge)
|
79
|
+
else
|
80
|
+
new_record = associated_class.create!(related_record_data['attributes'])
|
81
|
+
restore_associations(new_record, related_record_data['associations'], merge: merge)
|
82
|
+
end
|
78
83
|
end
|
79
84
|
else
|
80
|
-
# Restore belongs_to or has_one association
|
81
85
|
associated_class = record.class.reflect_on_association(association_name).klass
|
82
|
-
|
83
|
-
|
86
|
+
if merge && related_data['attributes']['id']
|
87
|
+
existing_record = record.send(association_name)
|
88
|
+
|
89
|
+
if existing_record && existing_record.id == related_data['attributes']['id']
|
90
|
+
existing_record.update!(related_data['attributes'])
|
91
|
+
restore_associations(existing_record, related_data['associations'], merge: merge)
|
92
|
+
else
|
93
|
+
new_record = associated_class.create!(related_data['attributes'])
|
94
|
+
record.update!(association_name => new_record)
|
95
|
+
restore_associations(new_record, related_data['associations'], merge: merge)
|
96
|
+
end
|
97
|
+
else
|
98
|
+
new_record = associated_class.create!(related_data['attributes'])
|
99
|
+
record.update!(association_name => new_record)
|
100
|
+
restore_associations(new_record, related_data['associations'], merge: merge)
|
101
|
+
end
|
84
102
|
end
|
85
103
|
end
|
86
104
|
end
|
data/test/test_active_capture.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'test_helper'
|
2
|
+
require 'minitest/autorun'
|
3
3
|
|
4
4
|
class ActiveCaptureTest < Minitest::Test
|
5
5
|
def setup
|
@@ -9,36 +9,30 @@ class ActiveCaptureTest < Minitest::Test
|
|
9
9
|
@comment2 = @post.comments.create!(content: "Thanks for sharing.")
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
ActiveCapture::
|
12
|
+
def test_capture_with_associations
|
13
|
+
ActiveCapture::Capture.take(@user, associations: [:posts, { posts: :comments }])
|
14
14
|
|
15
|
-
|
16
|
-
assert !
|
15
|
+
capture_files = Dir["captures/user/*.json"]
|
16
|
+
assert !capture_files.empty?, "capture file should be created"
|
17
17
|
|
18
|
-
|
19
|
-
assert_equal 1,
|
20
|
-
assert_equal 2,
|
18
|
+
capture_content = JSON.parse(File.read(capture_files.last))
|
19
|
+
assert_equal 1, capture_content["associations"]["posts"].size
|
20
|
+
assert_equal 2, capture_content["associations"]["posts"].first["associations"]["comments"].size
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
ActiveCapture::
|
25
|
-
|
23
|
+
def test_restore_with_merge
|
24
|
+
ActiveCapture::Capture.take(@user, associations: [:posts, { posts: :comments }])
|
25
|
+
capture_file = Dir["captures/user/*.json"].last
|
26
|
+
@post.update(title: "updated title")
|
27
|
+
new_comment = @post.comments.create!(content: "Another comment")
|
28
|
+
ActiveCapture::Capture.restore(@user, capture_file, merge: true)
|
26
29
|
|
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
30
|
@user.reload
|
37
31
|
@post.reload
|
38
32
|
|
39
|
-
# Assertions
|
40
33
|
assert_equal "John Doe", @user.name
|
41
34
|
assert_equal "First Post", @post.title
|
42
|
-
assert_equal
|
35
|
+
assert_equal 3, @post.comments.count
|
36
|
+
assert @post.comments.exists?(new_comment.id), "New comment should still exist after merge"
|
43
37
|
end
|
44
38
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_capture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tanmay Bhawsar
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-31 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: minitest
|
@@ -23,7 +23,7 @@ dependencies:
|
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '5.0'
|
26
|
-
description: ActiveCapture allows you to save
|
26
|
+
description: ActiveCapture allows you to save captures of ActiveRecord records, including
|
27
27
|
their nested associations, and restore them at any point. Useful for auditing, backups,
|
28
28
|
and rollback functionality.
|
29
29
|
email: bhawsartanmay@gmail.com
|
@@ -32,7 +32,7 @@ extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
34
|
- lib/active_capture.rb
|
35
|
-
- lib/active_capture/
|
35
|
+
- lib/active_capture/capture_storage.rb
|
36
36
|
- lib/active_capture/version.rb
|
37
37
|
- test/test_active_capture.rb
|
38
38
|
- test/test_helper.rb
|
@@ -58,6 +58,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements: []
|
59
59
|
rubygems_version: 3.6.2
|
60
60
|
specification_version: 4
|
61
|
-
summary: A Ruby on Rails gem for taking and restoring
|
61
|
+
summary: A Ruby on Rails gem for taking and restoring captures of ActiveRecord records
|
62
62
|
with nested associations.
|
63
63
|
test_files: []
|
@@ -1,23 +0,0 @@
|
|
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
|