undestroy 0.0.2 → 0.1.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.
- data/ARCH.md +13 -24
- data/README.md +72 -27
- data/lib/undestroy/binding.rb +1 -0
- data/lib/undestroy/binding/active_record.rb +53 -11
- data/lib/undestroy/binding/active_record/migration_statement.rb +118 -0
- data/lib/undestroy/binding/active_record/restorable.rb +28 -0
- data/lib/undestroy/config.rb +32 -8
- data/lib/undestroy/config/field.rb +20 -0
- data/lib/undestroy/restore.rb +44 -0
- data/lib/undestroy/version.rb +1 -1
- data/lib/undestroy/without_binding.rb +1 -0
- data/test/fixtures/ar.rb +8 -4
- data/test/fixtures/load_test/models1/test_model001.rb +2 -0
- data/test/fixtures/load_test/models1/test_model002.rb +2 -0
- data/test/fixtures/load_test/models1/test_module001.rb +2 -0
- data/test/fixtures/load_test/models1/test_module001/test_model003.rb +2 -0
- data/test/fixtures/load_test/models2/test_model001.rb +2 -0
- data/test/fixtures/load_test/models2/test_model002.rb +2 -0
- data/test/fixtures/load_test/models2/test_module001.rb +2 -0
- data/test/fixtures/load_test/models2/test_module001/test_model003.rb +2 -0
- data/test/helper.rb +12 -0
- data/test/helpers/model_loading.rb +20 -0
- data/test/integration/active_record_test.rb +50 -1
- data/test/irb.rb +2 -0
- data/test/unit/archive_test.rb +1 -1
- data/test/unit/binding/active_record/migration_statement_test.rb +306 -0
- data/test/unit/binding/active_record/restorable_test.rb +132 -0
- data/test/unit/binding/active_record_test.rb +187 -19
- data/test/unit/config/field_test.rb +86 -0
- data/test/unit/config_test.rb +114 -8
- data/test/unit/restore_test.rb +95 -0
- metadata +35 -3
data/ARCH.md
CHANGED
@@ -6,32 +6,18 @@ modular and easy to tailor to your specific needs.
|
|
6
6
|
### `Config`
|
7
7
|
|
8
8
|
Holds configuration information for Undestroy. An instance is created
|
9
|
-
globally and serves as defaults for each model using Undestroy
|
10
|
-
model
|
11
|
-
override any of the globally configurable options.
|
9
|
+
globally and serves as defaults for each model using Undestroy, but each
|
10
|
+
model has its own unique configuration allowing developer flexibility.
|
12
11
|
|
13
|
-
|
12
|
+
Each of the core classes `Archive`, `Restore`, and `Transfer` can be
|
13
|
+
configured in the `:internals` hash option on a per model basis allowing
|
14
|
+
the developer to provide custom classes for the various actions
|
15
|
+
Undestroy provides.
|
14
16
|
|
15
|
-
```ruby
|
16
|
-
Undestroy::Config.configure do |config|
|
17
|
-
config.abstract_class = ArchiveModel
|
18
|
-
config.fields = {
|
19
|
-
:deleted_at => proc { Time.now },
|
20
|
-
:deleted_by_id => proc { User.current.id if User.current }
|
21
|
-
}
|
22
|
-
end
|
23
|
-
```
|
24
|
-
|
25
|
-
This changes the default abstract class from ActiveRecord::Base to a
|
26
|
-
model called ArchiveModel. This also sets the default fields to include
|
27
|
-
a deleted_by_id which automatically sets the current user as the deleter
|
28
|
-
of the record.
|
29
|
-
|
30
|
-
Possible configuration options are listed in the _Usage_ section above.
|
31
17
|
|
32
18
|
### `Archive`
|
33
19
|
|
34
|
-
Map the source model's schema to the
|
20
|
+
Map the source model's schema to the target model's and initiate the
|
35
21
|
transfer through `Transfer`. When `run` is called the Transfer is
|
36
22
|
initialized with a primitive hash mapping the schema to the archive
|
37
23
|
table.
|
@@ -44,7 +30,9 @@ Initialized with:
|
|
44
30
|
### `Restore`
|
45
31
|
|
46
32
|
Map the archive model's schema to the source model's and initiate the
|
47
|
-
transfer through `Transfer`
|
33
|
+
transfer through `Transfer` When `run` is called the `Transfer` is
|
34
|
+
initialized with a primitive hash mapping the schema from the archive
|
35
|
+
table to the source table.
|
48
36
|
|
49
37
|
Initialized with:
|
50
38
|
|
@@ -71,14 +59,15 @@ is bound to the `before_destroy` callback that performs the archiving
|
|
71
59
|
functions. Any of the code that handles ActiveRecord specific logic
|
72
60
|
lives in here.
|
73
61
|
|
74
|
-
Initialized with: *
|
62
|
+
Initialized with: *Options for `Config` class*
|
75
63
|
|
76
64
|
Attributes:
|
77
65
|
|
66
|
+
* `model`: The AR model ths instance binds
|
78
67
|
* `config`: Returns this model's config object
|
79
|
-
* `model`: The AR model this instnace was created for
|
80
68
|
|
81
69
|
Methods:
|
82
70
|
|
83
71
|
* `before_destroy`: Perform the archive process
|
72
|
+
* `self.add(klass)`: Performs patch to provided klass needed for binding
|
84
73
|
|
data/README.md
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# Undestroy
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
Provides automatic archiving of ActiveRecord models before the object is
|
4
|
+
destroyed. It is designed to be database agnostic and easy to extend
|
5
|
+
with custom functionality. Migrations will be run in parallel on the
|
6
|
+
archive table when run on the original table. Additional archive schema
|
7
|
+
can be appended to the archive table for storing tracking data (default:
|
8
|
+
deleted_at).
|
9
|
+
|
9
10
|
|
10
11
|
## Installation
|
11
12
|
|
@@ -41,23 +42,48 @@ class Person < ActiveRecord::Base
|
|
41
42
|
end
|
42
43
|
```
|
43
44
|
|
44
|
-
This method can also accept an options hash to further
|
45
|
-
Undestroy to your needs.
|
45
|
+
This method can also accept an options hash or block to further
|
46
|
+
customize Undestroy to your needs. Here are some of the common options:
|
46
47
|
|
47
48
|
* `:table_name`: use this table for archiving (Defaults to the
|
48
|
-
source class's table_name prefixed with
|
49
|
+
source class's table_name prefixed with the :prefix option).
|
50
|
+
* `:prefix`: use this prefix for table names -- if :table_name is set
|
51
|
+
this option does nothing (default: "archive_")
|
49
52
|
* `:abstract_class`: use this as the base class for the target_class
|
50
|
-
specify an alternate for custom extensions
|
51
|
-
|
52
|
-
* `:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
* `:
|
57
|
-
|
53
|
+
specify an alternate for custom extensions (defaults to
|
54
|
+
ActiveRecord::Base)
|
55
|
+
* `:migrate`: Determines whether Undestroy will handle automatic
|
56
|
+
migrations (default: true)
|
57
|
+
* `:indexes`: When :migrate is true should indexes be migrated as well?
|
58
|
+
(default: false)
|
59
|
+
* `add_field(name, type, value=nil, &block)`: method on the Config
|
60
|
+
object that configures a new field for the archive table. The return
|
61
|
+
value of the block or value of `value` is used as the value of the
|
62
|
+
field. The block will be passed the instance of the object to be
|
63
|
+
archived as an argument.
|
64
|
+
|
65
|
+
You can also use a block to handle the configuration:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class Post < ActiveRecord::Base
|
69
|
+
undestroy do |config|
|
70
|
+
config.prefix = "old_"
|
71
|
+
config.add_field :deleted_by_id, :integer do |instance|
|
72
|
+
User.current.id if User.current
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
58
77
|
|
59
|
-
|
78
|
+
Advanced Options:
|
60
79
|
|
80
|
+
* `:fields`: Specify a hash of Field objects describing additional
|
81
|
+
fields you would like to include on the archive table. The preferred
|
82
|
+
method of specificying fields is through the add_field method with the
|
83
|
+
block configuration method. (defaults to deleted_at timestamp).
|
84
|
+
* `:model_paths`: Array of paths where Undestroy models live. This is
|
85
|
+
used to autoload models before migrations are run (default:
|
86
|
+
`Rails.root.join('app', 'models')`).
|
61
87
|
* `:source_class`: the AR model of the originating data. Set
|
62
88
|
automatically to class `undestroy` method is called on.
|
63
89
|
* `:target_class`: use this AR model for archiving. Set automatically
|
@@ -66,11 +92,19 @@ Internal Options (for advanced users):
|
|
66
92
|
keys are `:archive`, `:transfer` and `:restore`. Defaults to
|
67
93
|
corresponding internal classes. Customize to your heart's content.
|
68
94
|
|
69
|
-
```
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
# =>
|
95
|
+
```ruby
|
96
|
+
person = Person.create(:name => "Billy Mcgeeferson")
|
97
|
+
# Creates a new person record in people table
|
98
|
+
person.destroy
|
99
|
+
# => Inserts record in archive_people table
|
100
|
+
# => Deletes record from people table
|
101
|
+
People.restore(person.id)
|
102
|
+
People.archived.where(:name => "Billy Mcgeeferson").restore_all
|
103
|
+
# => Two ways to restore the record back to the people table
|
104
|
+
People.archived.find(person.id).restore_copy
|
105
|
+
# => Restores the record, but doesn't remove the archived record
|
106
|
+
People.find(person.id).destroy!
|
107
|
+
# => Destroys the record without archiving
|
74
108
|
```
|
75
109
|
|
76
110
|
## Configuring
|
@@ -81,10 +115,9 @@ configuration block in your application initializer:
|
|
81
115
|
```ruby
|
82
116
|
Undestroy::Config.configure do |config|
|
83
117
|
config.abstract_class = ArchiveModelBase
|
84
|
-
config.
|
85
|
-
|
86
|
-
|
87
|
-
}
|
118
|
+
config.add_field :deleted_by_id, :datetime do |instance|
|
119
|
+
User.current.id if User.current
|
120
|
+
end
|
88
121
|
end
|
89
122
|
```
|
90
123
|
|
@@ -92,6 +125,18 @@ Options set in this block will be the default for all models with
|
|
92
125
|
undestroy activated. They can be overriden with options passed to the
|
93
126
|
`undestroy` method
|
94
127
|
|
128
|
+
## Architecture
|
129
|
+
|
130
|
+
Checkout the ARCH.md file for docs on how the gem is setup internally,
|
131
|
+
or just read the code. My goal was to make it easy to understand and
|
132
|
+
extend.
|
133
|
+
|
134
|
+
Enjoy!
|
135
|
+
|
136
|
+
## Acknowledgements
|
137
|
+
|
138
|
+
* acts_as_archive author Winton Welsh for inspiration
|
139
|
+
|
95
140
|
## Contributing
|
96
141
|
|
97
142
|
1. Fork it
|
data/lib/undestroy/binding.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
|
3
3
|
class Undestroy::Binding::ActiveRecord
|
4
|
-
|
4
|
+
|
5
|
+
attr_accessor :config, :model, :active
|
6
|
+
alias :active? :active
|
5
7
|
|
6
8
|
def initialize(model, options={})
|
7
9
|
ensure_is_ar! model
|
8
10
|
|
9
11
|
self.model = model
|
10
12
|
self.config = Undestroy::Config.config.merge(options)
|
13
|
+
yield self.config if block_given?
|
14
|
+
self.active = true
|
11
15
|
|
12
16
|
set_defaults
|
13
17
|
end
|
@@ -16,11 +20,23 @@ class Undestroy::Binding::ActiveRecord
|
|
16
20
|
config.internals[:archive].new(:config => config, :source => instance).run
|
17
21
|
end
|
18
22
|
|
23
|
+
def prefix_table_name(name)
|
24
|
+
self.config.prefix.to_s + name.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def deactivated
|
28
|
+
previously = self.active
|
29
|
+
self.active = false
|
30
|
+
yield
|
31
|
+
ensure
|
32
|
+
self.active = previously
|
33
|
+
end
|
34
|
+
|
19
35
|
protected
|
20
36
|
|
21
37
|
def set_defaults
|
22
38
|
self.config.source_class = self.model
|
23
|
-
self.config.table_name ||=
|
39
|
+
self.config.table_name ||= prefix_table_name(self.model.table_name) if self.model.respond_to?(:table_name)
|
24
40
|
self.config.target_class ||= create_target_class
|
25
41
|
ensure_is_ar! self.config.target_class
|
26
42
|
end
|
@@ -29,13 +45,12 @@ class Undestroy::Binding::ActiveRecord
|
|
29
45
|
def create_target_class
|
30
46
|
Class.new(self.config.abstract_class || ActiveRecord::Base).tap do |target_class|
|
31
47
|
target_class.table_name = self.config.table_name
|
48
|
+
target_class.class_attribute :undestroy_model_binding, :instance_writer => false
|
49
|
+
target_class.undestroy_model_binding = self
|
50
|
+
target_class.send :include, Restorable
|
32
51
|
end
|
33
52
|
end
|
34
53
|
|
35
|
-
def table_prefix
|
36
|
-
"archive_"
|
37
|
-
end
|
38
|
-
|
39
54
|
def ensure_is_ar!(klass)
|
40
55
|
raise ArgumentError, "#{klass.inspect} must be an ActiveRecord model" unless is_ar?(klass)
|
41
56
|
end
|
@@ -47,17 +62,44 @@ class Undestroy::Binding::ActiveRecord
|
|
47
62
|
# Add binding to the given class if it doesn't already have it
|
48
63
|
def self.add(klass=ActiveRecord::Base)
|
49
64
|
klass.class_eval do
|
50
|
-
class_attribute :undestroy_model_binding, :instance_writer => false
|
51
65
|
|
52
66
|
def self.undestroy(options={})
|
53
|
-
|
54
|
-
|
55
|
-
|
67
|
+
class_eval do
|
68
|
+
class_attribute :undestroy_model_binding, :instance_writer => false
|
69
|
+
|
70
|
+
before_destroy { undestroy_model_binding.before_destroy(self) }
|
71
|
+
|
72
|
+
def self.archived
|
73
|
+
undestroy_model_binding.config.target_class
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.restore(*args)
|
77
|
+
[*archived.find(*args)].each(&:restore)
|
78
|
+
end
|
79
|
+
|
80
|
+
def destroy!
|
81
|
+
undestroy_model_binding.deactivated do
|
82
|
+
destroy
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end unless respond_to?(:undestroy_model_binding)
|
87
|
+
|
56
88
|
self.undestroy_model_binding = Undestroy::Binding::ActiveRecord.new(self, options)
|
57
89
|
end
|
58
90
|
|
59
|
-
|
91
|
+
|
92
|
+
end unless klass.respond_to?(:undestroy)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.load_models(path)
|
96
|
+
Dir[File.join(path, '**', '*.rb')].each do |file|
|
97
|
+
require_dependency file
|
98
|
+
end
|
60
99
|
end
|
61
100
|
|
62
101
|
end
|
63
102
|
|
103
|
+
require 'undestroy/binding/active_record/migration_statement'
|
104
|
+
require 'undestroy/binding/active_record/restorable'
|
105
|
+
|
@@ -0,0 +1,118 @@
|
|
1
|
+
|
2
|
+
class Undestroy::Binding::ActiveRecord::MigrationStatement
|
3
|
+
|
4
|
+
SCHEMA = [
|
5
|
+
:create_table, :drop_table, :rename_table,
|
6
|
+
:add_column, :rename_column, :change_column, :remove_column,
|
7
|
+
]
|
8
|
+
|
9
|
+
INDEX = [
|
10
|
+
:add_index, :remove_index
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_accessor :method_name, :arguments, :block
|
14
|
+
|
15
|
+
def self.add(klass=ActiveRecord::Migration)
|
16
|
+
klass.class_eval do
|
17
|
+
|
18
|
+
def method_missing_with_undestroy(method, *args, &block)
|
19
|
+
original = method(:method_missing_without_undestroy)
|
20
|
+
original.call method, *args, &block
|
21
|
+
stmt = Undestroy::Binding::ActiveRecord::MigrationStatement.new(method, *args, &block)
|
22
|
+
stmt.run!(original) if stmt.run?
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :method_missing_without_undestroy :method_missing
|
26
|
+
alias :method_missing :method_missing_with_undestroy
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(method_name, *args, &block)
|
32
|
+
self.class.ensure_models_loaded
|
33
|
+
self.method_name = method_name
|
34
|
+
self.arguments = args
|
35
|
+
self.block = block
|
36
|
+
end
|
37
|
+
|
38
|
+
def source_table_name
|
39
|
+
self.arguments[0]
|
40
|
+
end
|
41
|
+
|
42
|
+
def target_table_name
|
43
|
+
config.target_class.table_name if config
|
44
|
+
end
|
45
|
+
|
46
|
+
def config
|
47
|
+
Undestroy::Config.catalog.select do |c|
|
48
|
+
c.source_class.respond_to?(:table_name) &&
|
49
|
+
c.source_class.table_name.to_s == source_table_name.to_s
|
50
|
+
end.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def target_arguments
|
54
|
+
self.arguments.dup.tap do |args|
|
55
|
+
args[0] = target_table_name
|
56
|
+
args[1] = binding.prefix_table_name(args[1]) if rename_table?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def schema_action?
|
61
|
+
SCHEMA.include?(method_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def index_action?
|
65
|
+
INDEX.include?(method_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
def run?
|
69
|
+
(
|
70
|
+
arguments.present? &&
|
71
|
+
config && config.migrate &&
|
72
|
+
!rename_table_exception? &&
|
73
|
+
(
|
74
|
+
schema_action? ||
|
75
|
+
index_action? && config.indexes
|
76
|
+
)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def run!(callable)
|
81
|
+
callable.call(method_name, *target_arguments, &block)
|
82
|
+
|
83
|
+
if create_table?
|
84
|
+
config.fields.values.sort.each do |field|
|
85
|
+
callable.call(:add_column, target_table_name, field.name, field.type)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.ensure_models_loaded
|
91
|
+
Undestroy::Config.config.model_paths.each do |path|
|
92
|
+
Undestroy::Binding::ActiveRecord.load_models(path)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
# We don't want to run rename_table on the target when the table name is
|
99
|
+
# explicitly set in the configuration. The user must do manual migrating
|
100
|
+
# in that case.
|
101
|
+
def rename_table_exception?
|
102
|
+
rename_table? && config.table_name
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_table?
|
106
|
+
method_name == :create_table
|
107
|
+
end
|
108
|
+
|
109
|
+
def rename_table?
|
110
|
+
method_name == :rename_table
|
111
|
+
end
|
112
|
+
|
113
|
+
def binding
|
114
|
+
config.source_class.undestroy_model_binding
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Undestroy::Binding::ActiveRecord::Restorable
|
2
|
+
|
3
|
+
def restore
|
4
|
+
restore_copy
|
5
|
+
destroy
|
6
|
+
end
|
7
|
+
|
8
|
+
def restore_copy
|
9
|
+
config = undestroy_model_binding.config
|
10
|
+
config.internals[:restore].new(:target => self, :config => config).run
|
11
|
+
end
|
12
|
+
|
13
|
+
module RelationExtensions
|
14
|
+
|
15
|
+
def restore_all
|
16
|
+
to_a.collect { |record| record.restore }.tap { reset }
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.included(receiver)
|
22
|
+
receiver.send(:relation).singleton_class.class_eval do
|
23
|
+
include Undestroy::Binding::ActiveRecord::Restorable::RelationExtensions
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|