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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6e10fd5af7be677b0441dd76aaa567955953ba576753b10e3130daaff2b01d0
4
- data.tar.gz: 4506c04f93aefb84f841dc9c591ccebe5dd24c3199f9056e9b39999cf4184c2e
3
+ metadata.gz: 874ca90fb90863834525adb945cfead2334c37dcf115b3768c5c1500ac8cc85f
4
+ data.tar.gz: 888263dff4492a4b952e1dc491a49d0cda55c31777c7ee1db3de3e0df6823b89
5
5
  SHA512:
6
- metadata.gz: f3ce3ab62a22da2e1c0e34cfc1ed13081f8a477facdfff0d3d13cb1f6aaa140981300d67f108afc1ccb99875edc0443aaf45569c0c3bb5eb71c6068339683dd0
7
- data.tar.gz: 79e418aab36f10f116120753cc4962e2a9933d22649292a424afc6efc89945ee5f95230701c3d3d1ebc534914523af3bd4e787bf0052eb6fd81ef6b3cf4d81db
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCapture
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -1,31 +1,32 @@
1
- require "active_capture/version"
2
- require_relative "active_capture/snapshot_storage"
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 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)
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
- snapshot_data = {
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, include_associations)
15
+ associations: capture_associations(record, associations)
14
16
  }
15
17
 
16
- SnapshotStorage.save_snapshot(record, snapshot_data, name)
18
+ CaptureStorage.save_capture(record, capture_data, name)
17
19
  end
18
20
 
19
- def self.restore(record, snapshot_file)
20
- snapshot_data = JSON.parse(File.read(snapshot_file))
21
+ def self.restore(record, capture_file, merge: false)
22
+ captured_records = CaptureStorage.load_capture(capture_file)
21
23
 
22
- raise ArgumentError, "Snapshot file does not match the given record" unless snapshot_data["record_id"] == record.id
24
+ raise ArgumentError, 'capture file does not match the given record' unless captured_records['record_id'] == record.id
23
25
 
24
- # Restore record attributes
25
- record.update!(snapshot_data["attributes"])
26
-
27
- # Restore associations
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).destroy_all
73
+ existing_records = record.send(association_name)
74
74
 
75
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"])
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
- associated_record = associated_class.create!(related_data["attributes"])
83
- restore_associations(associated_record, related_data["associations"])
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
@@ -1,5 +1,5 @@
1
- require "test_helper"
2
- require "minitest/autorun"
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 test_snapshot_with_nested_associations
13
- ActiveCapture::Snapshot.take(@user, include_associations: [{ posts: :comments }])
12
+ def test_capture_with_associations
13
+ ActiveCapture::Capture.take(@user, associations: [:posts, { posts: :comments }])
14
14
 
15
- snapshot_files = Dir["snapshots/user/*.json"]
16
- assert !snapshot_files.empty?, "Snapshot file should be created"
15
+ capture_files = Dir["captures/user/*.json"]
16
+ assert !capture_files.empty?, "capture file should be created"
17
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
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 test_restore_from_snapshot
24
- ActiveCapture::Snapshot.take(@user, include_associations: [{ posts: :comments }])
25
- snapshot_file = Dir["snapshots/user/*.json"].last
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 2, @post.comments.count
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.1.1
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-29 00:00:00.000000000 Z
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 snapshots of ActiveRecord records, including
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/snapshot_storage.rb
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 snapshots of ActiveRecord records
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