undestroy 0.0.1 → 0.0.2
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 +84 -0
- data/Gemfile +2 -1
- data/LICENSE +1 -1
- data/README.md +58 -48
- data/lib/undestroy.rb +3 -4
- data/lib/undestroy/archive.rb +36 -0
- data/lib/undestroy/binding.rb +8 -0
- data/lib/undestroy/binding/active_record.rb +63 -0
- data/lib/undestroy/config.rb +54 -0
- data/lib/undestroy/transfer.rb +22 -0
- data/lib/undestroy/version.rb +2 -1
- data/lib/undestroy/with_binding.rb +5 -0
- data/lib/undestroy/without_binding.rb +10 -0
- data/test/fixtures/active_record_models.rb +42 -0
- data/test/fixtures/ar.rb +41 -0
- data/test/fixtures/archive.rb +21 -0
- data/test/helper.rb +42 -1
- data/test/integration/active_record_test.rb +180 -0
- data/test/unit/archive_test.rb +115 -0
- data/test/unit/binding/active_record_test.rb +188 -0
- data/test/unit/config_test.rb +146 -0
- data/test/unit/transfer_test.rb +65 -0
- data/undestroy.gemspec +2 -1
- metadata +29 -6
- data/test/unit/undestroy_test.rb +0 -10
data/ARCH.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Undestroy Model Structure
|
2
|
+
|
3
|
+
This is the basic class structure of this gem. It was designed to be
|
4
|
+
modular and easy to tailor to your specific needs.
|
5
|
+
|
6
|
+
### `Config`
|
7
|
+
|
8
|
+
Holds configuration information for Undestroy. An instance is created
|
9
|
+
globally and serves as defaults for each model using Undestroy. Each
|
10
|
+
model also creates its own instance of Config allowing any model to
|
11
|
+
override any of the globally configurable options.
|
12
|
+
|
13
|
+
To change global defaults use this configuration DSL:
|
14
|
+
|
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
|
+
|
32
|
+
### `Archive`
|
33
|
+
|
34
|
+
Map the source model's schema to the archive model's and initiate the
|
35
|
+
transfer through `Transfer`. When `run` is called the Transfer is
|
36
|
+
initialized with a primitive hash mapping the schema to the archive
|
37
|
+
table.
|
38
|
+
|
39
|
+
Initialized with:
|
40
|
+
|
41
|
+
* `:config`: Instance of Undestroy::Config for this model
|
42
|
+
* `:source`: Instance of the source model
|
43
|
+
|
44
|
+
### `Restore`
|
45
|
+
|
46
|
+
Map the archive model's schema to the source model's and initiate the
|
47
|
+
transfer through `Transfer`
|
48
|
+
|
49
|
+
Initialized with:
|
50
|
+
|
51
|
+
* `:config`: Instance of Undestroy::Config for this model
|
52
|
+
* `:archive`: Instance of the archived model
|
53
|
+
|
54
|
+
### `Transfer`
|
55
|
+
|
56
|
+
Handles the actual movement of data from one table to another. This
|
57
|
+
class simply uses the AR interface to create and delete the appropriate
|
58
|
+
records. This can be subclassed to provide enhanced performance or
|
59
|
+
customized behavior for your situation.
|
60
|
+
|
61
|
+
Initialized with:
|
62
|
+
|
63
|
+
* `:fields`: Hash of field names to values to be stored in this table
|
64
|
+
* `:klass`: Target AR model which will be created with these attributes
|
65
|
+
|
66
|
+
### `Binding::ActiveRecord`
|
67
|
+
|
68
|
+
Binds the base functionality to ActiveRecord. It is initialized by the
|
69
|
+
parameters to the `undestroy` class method and contains the method that
|
70
|
+
is bound to the `before_destroy` callback that performs the archiving
|
71
|
+
functions. Any of the code that handles ActiveRecord specific logic
|
72
|
+
lives in here.
|
73
|
+
|
74
|
+
Initialized with: *Config options from above*
|
75
|
+
|
76
|
+
Attributes:
|
77
|
+
|
78
|
+
* `config`: Returns this model's config object
|
79
|
+
* `model`: The AR model this instnace was created for
|
80
|
+
|
81
|
+
Methods:
|
82
|
+
|
83
|
+
* `before_destroy`: Perform the archive process
|
84
|
+
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
Allow copying records to alternate table before destroying an
|
4
4
|
ActiveRecord model for archiving purposes. Data will be mapped
|
5
5
|
one-to-one to the archive table schema. Additional fields can also be
|
6
|
-
configured for additional tracking information. Archive table schema
|
6
|
+
configured for additional tracking information. -Archive table schema
|
7
7
|
will automatically be updated when the parent model's table is migrated
|
8
|
-
through Rails
|
8
|
+
through Rails.- (not yet)
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -21,21 +21,34 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
$ gem install undestroy
|
23
23
|
|
24
|
+
You can also tell Undestroy to not extend ActiveRecord when required by
|
25
|
+
using this line in your Gemfile instead:
|
26
|
+
|
27
|
+
gem 'undestroy', :require => 'undestroy/without_binding'
|
28
|
+
|
29
|
+
If you do this you must call
|
30
|
+
`Undestroy::Binding::ActiveRecord.add(MyARSubclass)` where
|
31
|
+
`MYARSubclass` is the class you want Undestroy to extend instead.
|
32
|
+
|
24
33
|
## Usage
|
25
34
|
|
26
|
-
To activate Undestroy on a model, simply call the `
|
35
|
+
To activate Undestroy on a model, simply call the `undestroy` method
|
27
36
|
on the class like so:
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
38
|
+
```ruby
|
39
|
+
class Person < ActiveRecord::Base
|
40
|
+
undestroy
|
41
|
+
end
|
42
|
+
```
|
32
43
|
|
33
|
-
This method also
|
44
|
+
This method can also accept an options hash to further customize
|
34
45
|
Undestroy to your needs.
|
35
46
|
|
36
|
-
* `:table_name`: use this table for archiving
|
37
|
-
|
38
|
-
* `:
|
47
|
+
* `:table_name`: use this table for archiving (Defaults to the
|
48
|
+
source class's table_name prefixed with "archive_").
|
49
|
+
* `:abstract_class`: use this as the base class for the target_class
|
50
|
+
specify an alternate for custom extensions / DB connections (defaults
|
51
|
+
to ActiveRecord::Base)
|
39
52
|
* `:fields`: Specify a hash of fields to values for additional fields
|
40
53
|
you would like to include on the archive table -- lambdas will be
|
41
54
|
called with the instance being destroyed and returned value will be
|
@@ -43,44 +56,41 @@ Undestroy to your needs.
|
|
43
56
|
* `:migrate`: Should Undestroy migrate the archive table together with
|
44
57
|
this model's table (default: true)
|
45
58
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
class simply uses the AR interface to create and delete the appropriate
|
82
|
-
records. This can be subclassed to provide enhanced performance or
|
83
|
-
customized behavior for your situation.
|
59
|
+
Internal Options (for advanced users):
|
60
|
+
|
61
|
+
* `:source_class`: the AR model of the originating data. Set
|
62
|
+
automatically to class `undestroy` method is called on.
|
63
|
+
* `:target_class`: use this AR model for archiving. Set automatically
|
64
|
+
to dynamically generated class based on `archive_*` options.
|
65
|
+
* `internals`: internal classes to use for archival process. Possible
|
66
|
+
keys are `:archive`, `:transfer` and `:restore`. Defaults to
|
67
|
+
corresponding internal classes. Customize to your heart's content.
|
68
|
+
|
69
|
+
```
|
70
|
+
$ person = Person.find(1)
|
71
|
+
$ person.destroy
|
72
|
+
# => Inserts person data into archive_people table
|
73
|
+
# => Deletes person data from people table
|
74
|
+
```
|
75
|
+
|
76
|
+
## Configuring
|
77
|
+
|
78
|
+
You can specify custom global configurations for Undestroy through a
|
79
|
+
configuration block in your application initializer:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Undestroy::Config.configure do |config|
|
83
|
+
config.abstract_class = ArchiveModelBase
|
84
|
+
config.fields = {
|
85
|
+
:deleted_at => proc { Time.now },
|
86
|
+
:deleted_by_id => proc { User.current.id if User.current }
|
87
|
+
}
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Options set in this block will be the default for all models with
|
92
|
+
undestroy activated. They can be overriden with options passed to the
|
93
|
+
`undestroy` method
|
84
94
|
|
85
95
|
## Contributing
|
86
96
|
|
data/lib/undestroy.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
class Undestroy::Archive
|
3
|
+
attr_accessor :source, :config, :transfer
|
4
|
+
|
5
|
+
def initialize(args={})
|
6
|
+
validate_arguments(args)
|
7
|
+
|
8
|
+
self.source = args[:source]
|
9
|
+
self.config = args[:config]
|
10
|
+
self.transfer = args[:transfer]
|
11
|
+
end
|
12
|
+
|
13
|
+
def transfer
|
14
|
+
@transfer ||= self.config.internals[:transfer].new(
|
15
|
+
:klass => self.config.target_class,
|
16
|
+
:fields => archive_fields
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
transfer.run
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def archive_fields
|
27
|
+
self.config.primitive_fields(self.source).merge(self.source.attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_arguments(args)
|
31
|
+
unless (args.keys & [:source, :config]).size == 2
|
32
|
+
raise ArgumentError, ":source and :config are required keys"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
class Undestroy::Binding::ActiveRecord
|
4
|
+
attr_accessor :config, :model
|
5
|
+
|
6
|
+
def initialize(model, options={})
|
7
|
+
ensure_is_ar! model
|
8
|
+
|
9
|
+
self.model = model
|
10
|
+
self.config = Undestroy::Config.config.merge(options)
|
11
|
+
|
12
|
+
set_defaults
|
13
|
+
end
|
14
|
+
|
15
|
+
def before_destroy(instance)
|
16
|
+
config.internals[:archive].new(:config => config, :source => instance).run
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def set_defaults
|
22
|
+
self.config.source_class = self.model
|
23
|
+
self.config.table_name ||= table_prefix + self.model.table_name if self.model.respond_to?(:table_name)
|
24
|
+
self.config.target_class ||= create_target_class
|
25
|
+
ensure_is_ar! self.config.target_class
|
26
|
+
end
|
27
|
+
|
28
|
+
# Builds a dynamic AR class representing the archival table
|
29
|
+
def create_target_class
|
30
|
+
Class.new(self.config.abstract_class || ActiveRecord::Base).tap do |target_class|
|
31
|
+
target_class.table_name = self.config.table_name
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_prefix
|
36
|
+
"archive_"
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_is_ar!(klass)
|
40
|
+
raise ArgumentError, "#{klass.inspect} must be an ActiveRecord model" unless is_ar?(klass)
|
41
|
+
end
|
42
|
+
|
43
|
+
def is_ar?(klass)
|
44
|
+
klass.is_a?(Class) && klass.ancestors.include?(ActiveRecord::Base)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add binding to the given class if it doesn't already have it
|
48
|
+
def self.add(klass=ActiveRecord::Base)
|
49
|
+
klass.class_eval do
|
50
|
+
class_attribute :undestroy_model_binding, :instance_writer => false
|
51
|
+
|
52
|
+
def self.undestroy(options={})
|
53
|
+
before_destroy do
|
54
|
+
self.undestroy_model_binding.before_destroy(self) if undestroy_model_binding
|
55
|
+
end unless self.undestroy_model_binding
|
56
|
+
self.undestroy_model_binding = Undestroy::Binding::ActiveRecord.new(self, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
end unless klass.respond_to?(:undestroy_model_binding)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Undestroy::Config
|
2
|
+
OPTIONS = [
|
3
|
+
:table_name, :abstract_class, :fields, :migrate,
|
4
|
+
:source_class, :target_class, :internals
|
5
|
+
]
|
6
|
+
attr_accessor *OPTIONS
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
self.migrate = true
|
10
|
+
self.fields = {
|
11
|
+
:deleted_at => proc { Time.now }
|
12
|
+
}
|
13
|
+
self.internals = {
|
14
|
+
:archive => Undestroy::Archive,
|
15
|
+
:transfer => Undestroy::Transfer,
|
16
|
+
}
|
17
|
+
|
18
|
+
options.each do |key, value|
|
19
|
+
self[key] = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
self.send(key) if OPTIONS.include?(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def []=(key, value)
|
28
|
+
self.send("#{key}=", value) if OPTIONS.include?(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
OPTIONS.inject({}) { |hash, key| hash.merge(key => self[key]) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def merge(object)
|
36
|
+
self.class.new(self.to_hash.merge(object.to_hash))
|
37
|
+
end
|
38
|
+
|
39
|
+
def primitive_fields(object)
|
40
|
+
self.fields.inject({}) do |hash, (key, val)|
|
41
|
+
hash.merge(key => val.is_a?(Proc) ? val.call(object) : val)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.configure
|
46
|
+
yield(config) if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.config
|
50
|
+
@config ||= self.new
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
class Undestroy::Transfer
|
3
|
+
attr_accessor :target
|
4
|
+
|
5
|
+
def initialize(args={})
|
6
|
+
raise ArgumentError, ":klass option required" unless args[:klass]
|
7
|
+
args[:fields] ||= {}
|
8
|
+
|
9
|
+
self.target = args[:klass].new
|
10
|
+
|
11
|
+
# Set instance values directly to avoid AR's filtering of protected fields
|
12
|
+
args[:fields].each do |field, value|
|
13
|
+
self.target[field] = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
self.target.save
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
data/lib/undestroy/version.rb
CHANGED