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.
Files changed (32) hide show
  1. data/ARCH.md +13 -24
  2. data/README.md +72 -27
  3. data/lib/undestroy/binding.rb +1 -0
  4. data/lib/undestroy/binding/active_record.rb +53 -11
  5. data/lib/undestroy/binding/active_record/migration_statement.rb +118 -0
  6. data/lib/undestroy/binding/active_record/restorable.rb +28 -0
  7. data/lib/undestroy/config.rb +32 -8
  8. data/lib/undestroy/config/field.rb +20 -0
  9. data/lib/undestroy/restore.rb +44 -0
  10. data/lib/undestroy/version.rb +1 -1
  11. data/lib/undestroy/without_binding.rb +1 -0
  12. data/test/fixtures/ar.rb +8 -4
  13. data/test/fixtures/load_test/models1/test_model001.rb +2 -0
  14. data/test/fixtures/load_test/models1/test_model002.rb +2 -0
  15. data/test/fixtures/load_test/models1/test_module001.rb +2 -0
  16. data/test/fixtures/load_test/models1/test_module001/test_model003.rb +2 -0
  17. data/test/fixtures/load_test/models2/test_model001.rb +2 -0
  18. data/test/fixtures/load_test/models2/test_model002.rb +2 -0
  19. data/test/fixtures/load_test/models2/test_module001.rb +2 -0
  20. data/test/fixtures/load_test/models2/test_module001/test_model003.rb +2 -0
  21. data/test/helper.rb +12 -0
  22. data/test/helpers/model_loading.rb +20 -0
  23. data/test/integration/active_record_test.rb +50 -1
  24. data/test/irb.rb +2 -0
  25. data/test/unit/archive_test.rb +1 -1
  26. data/test/unit/binding/active_record/migration_statement_test.rb +306 -0
  27. data/test/unit/binding/active_record/restorable_test.rb +132 -0
  28. data/test/unit/binding/active_record_test.rb +187 -19
  29. data/test/unit/config/field_test.rb +86 -0
  30. data/test/unit/config_test.rb +114 -8
  31. data/test/unit/restore_test.rb +95 -0
  32. 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. Each
10
- model also creates its own instance of Config allowing any model to
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
- To change global defaults use this configuration DSL:
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 archive model's and initiate 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: *Config options from above*
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
- Allow copying records to alternate table before destroying an
4
- ActiveRecord model for archiving purposes. Data will be mapped
5
- one-to-one to the archive table schema. Additional fields can also be
6
- configured for additional tracking information. -Archive table schema
7
- will automatically be updated when the parent model's table is migrated
8
- through Rails.- (not yet)
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 customize
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 "archive_").
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 / DB connections (defaults
51
- to ActiveRecord::Base)
52
- * `:fields`: Specify a hash of fields to values for additional fields
53
- you would like to include on the archive table -- lambdas will be
54
- called with the instance being destroyed and returned value will be
55
- used (default: `{ :deleted_at => proc { |instance| Time.now } }`).
56
- * `:migrate`: Should Undestroy migrate the archive table together with
57
- this model's table (default: true)
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
- Internal Options (for advanced users):
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
- $ person = Person.find(1)
71
- $ person.destroy
72
- # => Inserts person data into archive_people table
73
- # => Deletes person data from people table
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.fields = {
85
- :deleted_at => proc { Time.now },
86
- :deleted_by_id => proc { User.current.id if User.current }
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
@@ -3,6 +3,7 @@ module Undestroy::Binding
3
3
 
4
4
  def self.bind
5
5
  Undestroy::Binding::ActiveRecord.add(::ActiveRecord::Base)
6
+ Undestroy::Binding::ActiveRecord::MigrationStatement.add(::ActiveRecord::Migration)
6
7
  end
7
8
  end
8
9
 
@@ -1,13 +1,17 @@
1
1
  require 'active_record'
2
2
 
3
3
  class Undestroy::Binding::ActiveRecord
4
- attr_accessor :config, :model
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 ||= table_prefix + self.model.table_name if self.model.respond_to?(: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
- before_destroy do
54
- self.undestroy_model_binding.before_destroy(self) if undestroy_model_binding
55
- end unless self.undestroy_model_binding
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
- end unless klass.respond_to?(:undestroy_model_binding)
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
+