undestroy 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+