logidze 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/logidze.rb CHANGED
@@ -1,5 +1,25 @@
1
+ # frozen_string_literal: true
1
2
  require "logidze/version"
2
3
 
4
+ # Logidze provides tools for adding in-table JSON-based audit to DB tables
5
+ # and ActiveRecord extensions to work with changes history.
3
6
  module Logidze
4
- # Your code goes here...
7
+ require 'logidze/history'
8
+ require 'logidze/model'
9
+ require 'logidze/has_logidze'
10
+
11
+ require 'logidze/engine' if defined?(Rails)
12
+
13
+ # Temporary disable DB triggers.
14
+ #
15
+ # @example
16
+ # Logidze.without_logging { Post.update_all(active: true) }
17
+ def self.without_logging
18
+ ActiveRecord::Base.transaction do
19
+ ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled = 'on';"
20
+ res = yield
21
+ ActiveRecord::Base.connection.execute "SET LOCAL logidze.disabled = DEFAULT;"
22
+ res
23
+ end
24
+ end
5
25
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require 'logidze'
3
+
4
+ module Logidze
5
+ class Engine < Rails::Engine # :nodoc:
6
+ initializer "extend ActiveRecord with Logidze" do |_app|
7
+ ActiveSupport.on_load(:active_record) do
8
+ ActiveRecord::Base.send :include, Logidze::HasLogidze
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support'
3
+
4
+ module Logidze
5
+ # Add `has_logidze` method to AR::Base
6
+ module HasLogidze
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods # :nodoc:
10
+ # Include methods to work with history.
11
+ #
12
+ # rubocop:disable Style/PredicateName
13
+ def has_logidze
14
+ include Logidze::Model
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+ module Logidze
3
+ # Log data coder used for attribute serialization
4
+ class History
5
+ # History key
6
+ HISTORY = 'h'
7
+ # Version key
8
+ VERSION = 'v'
9
+
10
+ # Represents one log item
11
+ class Version
12
+ # Timestamp key
13
+ TS = 'ts'
14
+ # Changes key
15
+ CHANGES = 'c'
16
+
17
+ attr_reader :data
18
+
19
+ def initialize(data)
20
+ @data = data
21
+ end
22
+
23
+ def version
24
+ data.fetch(VERSION)
25
+ end
26
+
27
+ def changes
28
+ data.fetch(CHANGES)
29
+ end
30
+
31
+ def time
32
+ data.fetch(TS)
33
+ end
34
+ end
35
+
36
+ def self.dump(object)
37
+ ActiveSupport::JSON.encode(object)
38
+ end
39
+
40
+ def self.load(json)
41
+ new(json) unless json.nil?
42
+ end
43
+
44
+ attr_reader :data
45
+
46
+ delegate :size, to: :versions
47
+
48
+ def initialize(data)
49
+ @data = data
50
+ end
51
+
52
+ def versions
53
+ @versions ||= data.fetch(HISTORY).map { |v| Version.new(v) }
54
+ end
55
+
56
+ # Returns current version number
57
+ def version
58
+ data.fetch(VERSION)
59
+ end
60
+
61
+ # Change current version
62
+ def version=(val)
63
+ data.store(VERSION, val)
64
+ end
65
+
66
+ def current_version
67
+ find_by_version(version)
68
+ end
69
+
70
+ def previous_version
71
+ find_by_version(version - 1)
72
+ end
73
+
74
+ def next_version
75
+ find_by_version(version + 1)
76
+ end
77
+
78
+ # Return diff from the initial state to specified time or version.
79
+ # Optional `data` paramater can be used as initial diff state.
80
+ def changes_to(time: nil, version: nil, data: {}, from: 0)
81
+ raise ArgumentError, "Time or version must be specified" if time.nil? && version.nil?
82
+ filter = time.nil? ? method(:version_filter) : method(:time_filter)
83
+ versions.each_with_object(data.dup) do |v, acc|
84
+ next if v.version < from
85
+ break acc if filter.call(v, version, time)
86
+ acc.merge!(v.changes)
87
+ end
88
+ end
89
+
90
+ # Return diff object representing changes since specified time or version.
91
+ #
92
+ # @example
93
+ #
94
+ # diff_from(time: 2.days.ago)
95
+ # #=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }
96
+ # rubocop:disable Metrics/AbcSize
97
+ def diff_from(time: nil, version: nil)
98
+ raise ArgumentError, "Time or version must be specified" if time.nil? && version.nil?
99
+
100
+ from_version = version.nil? ? find_by_time(time) : find_by_version(version)
101
+ from_version ||= versions.first
102
+
103
+ base = changes_to(version: from_version.version)
104
+ diff = changes_to(version: self.version, data: base, from: from_version.version + 1)
105
+
106
+ build_changes(base, diff)
107
+ end
108
+ # rubocop:enable Metrics/AbcSize
109
+
110
+ # Return true iff time greater or equal to the first version time
111
+ def exists_ts?(time)
112
+ versions.present? && versions.first.time <= time
113
+ end
114
+
115
+ # Return true iff time corresponds to current version
116
+ def current_ts?(time)
117
+ (current_version.time <= time) &&
118
+ (next_version.nil? || (next_version.time < time))
119
+ end
120
+
121
+ # Return version by number or nil
122
+ def find_by_version(num)
123
+ versions.find { |v| v.version == num }
124
+ end
125
+
126
+ # Return nearest (from the bottom) version to the specified time
127
+ def find_by_time(time)
128
+ versions.reverse.find { |v| v.time <= time }
129
+ end
130
+
131
+ def ==(other)
132
+ return super unless other.is_a?(self.class)
133
+ data == other.data
134
+ end
135
+
136
+ def as_json(options = {})
137
+ data.as_json(options)
138
+ end
139
+
140
+ private
141
+
142
+ def build_changes(a, b)
143
+ b.each_with_object({}) do |kv, acc|
144
+ acc[kv.first] = { "old" => a[kv.first], "new" => kv.last } unless kv.last == a[kv.first]
145
+ end
146
+ end
147
+
148
+ def version_filter(item, version, _)
149
+ item.version > version
150
+ end
151
+
152
+ def time_filter(item, _, time)
153
+ item.time > time
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support'
3
+
4
+ module Logidze
5
+ # Extends model with methods to browse history
6
+ module Model
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ serialize :log_data, Logidze::History
11
+
12
+ delegate :version, :size, to: :log_data, prefix: "log"
13
+ end
14
+
15
+ module ClassMethods # :nodoc:
16
+ # Return records reverted to specified time
17
+ def at(ts)
18
+ all.map { |record| record.at(ts) }.compact
19
+ end
20
+
21
+ # Return changes made to records since specified time
22
+ def diff_from(ts)
23
+ all.map { |record| record.diff_from(ts) }
24
+ end
25
+
26
+ # Alias for Logidze.without_logging
27
+ def without_logging(&block)
28
+ Logidze.without_logging(&block)
29
+ end
30
+ end
31
+
32
+ # Use this to convert Ruby time to milliseconds
33
+ TIME_FACTOR = 1_000
34
+
35
+ # Return a dirty copy of record at specified time
36
+ # If time is less then the first version, then return nil.
37
+ # If time is greater then the last version, then return self.
38
+ def at(ts)
39
+ ts = parse_time(ts)
40
+ return nil unless log_data.exists_ts?(ts)
41
+ return self if log_data.current_ts?(ts)
42
+
43
+ object_at = dup
44
+ object_at.apply_diff(log_data.changes_to(time: ts))
45
+ end
46
+
47
+ # Revert record to the version at specified time (without saving to DB)
48
+ def at!(ts)
49
+ ts = parse_time(ts)
50
+ return self if log_data.current_ts?(ts)
51
+ return false unless log_data.exists_ts?(ts)
52
+
53
+ apply_diff(log_data.changes_to(time: ts))
54
+ end
55
+
56
+ # Return a dirty copy of specified version of record
57
+ def at_version(version)
58
+ return self if log_data.version == version
59
+ return nil unless log_data.find_by_version(version)
60
+
61
+ object_at = dup
62
+ object_at.apply_diff(log_data.changes_to(version: version))
63
+ end
64
+
65
+ # Revert record to the specified version (without saving to DB)
66
+ def at_version!(version)
67
+ return self if log_data.version == version
68
+ return false unless log_data.find_by_version(version)
69
+
70
+ apply_diff(log_data.changes_to(version: version))
71
+ end
72
+
73
+ # Return diff object representing changes since specified time.
74
+ #
75
+ # @example
76
+ #
77
+ # post.diff_from(2.days.ago)
78
+ # #=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }
79
+ def diff_from(ts)
80
+ ts = parse_time(ts)
81
+ { "id" => id, "changes" => log_data.diff_from(time: ts) }
82
+ end
83
+
84
+ # Restore record to the previous version.
85
+ # Return false if no previous version found, otherwise return updated record.
86
+ def undo!
87
+ version = log_data.previous_version
88
+ return false if version.nil?
89
+ switch_to!(version.version)
90
+ end
91
+
92
+ # Restore record to the _future_ version (if `undo!` was applied)
93
+ # Return false if no future version found, otherwise return updated record.
94
+ def redo!
95
+ version = log_data.next_version
96
+ return false if version.nil?
97
+ switch_to!(version.version)
98
+ end
99
+
100
+ # Restore record to the specified version.
101
+ # Return false if version is unknown.
102
+ def switch_to!(version)
103
+ return false unless at_version!(version)
104
+ log_data.version = version
105
+ self.class.without_logging { save! }
106
+ end
107
+
108
+ protected
109
+
110
+ def apply_diff(diff)
111
+ diff.each { |k, v| send("#{k}=", v) }
112
+ self
113
+ end
114
+
115
+ def parse_time(ts)
116
+ case ts
117
+ when Numeric
118
+ ts.to_i
119
+ when String
120
+ (Time.parse(ts).to_r * TIME_FACTOR).to_i
121
+ when Date
122
+ (ts.to_time.to_r * TIME_FACTOR).to_i
123
+ when Time
124
+ (ts.to_r * TIME_FACTOR).to_i
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Logidze
2
- VERSION = "0.0.1"
3
+ VERSION = "0.1.0"
3
4
  end
data/logidze.gemspec CHANGED
@@ -17,10 +17,15 @@ Gem::Specification.new do |spec|
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_runtime_dependency "activerecord", "~>4"
20
+ spec.add_dependency "rails", ">= 4.2.6"
21
21
 
22
22
  spec.add_development_dependency "pg", "~>0.18"
23
23
  spec.add_development_dependency "bundler", "~> 1.11"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "rspec", "~> 3.4"
25
+ spec.add_development_dependency "rspec", ">= 3.4"
26
+ spec.add_development_dependency "rspec-rails", ">= 3.4"
27
+ spec.add_development_dependency "database_cleaner", "~> 1.5"
28
+ spec.add_development_dependency "simplecov", ">= 0.3.8"
29
+ spec.add_development_dependency "ammeter", "~> 1.1.3"
30
+ spec.add_development_dependency "pry-byebug"
26
31
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logidze
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-13 00:00:00.000000000 Z
11
+ date: 2016-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: rails
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4'
19
+ version: 4.2.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4'
26
+ version: 4.2.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,16 +70,86 @@ dependencies:
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '3.4'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '3.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: database_cleaner
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 0.3.8
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 0.3.8
125
+ - !ruby/object:Gem::Dependency
126
+ name: ammeter
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.1.3
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.1.3
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-byebug
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
83
153
  description: PostgreSQL JSON-based auditing
84
154
  email:
85
155
  - dementiev.vm@gmail.com
@@ -91,15 +161,36 @@ files:
91
161
  - ".rspec"
92
162
  - ".rubocop.yml"
93
163
  - ".travis.yml"
164
+ - CHANGELOG.md
94
165
  - Gemfile
95
166
  - LICENSE.txt
96
167
  - README.md
97
168
  - Rakefile
98
169
  - bench/Makefile
170
+ - bench/Readme.md
99
171
  - bench/bench.sql
100
172
  - bench/hstore_trigger_setup.sql
173
+ - bench/jsonb_minus_2_setup.sql
174
+ - bench/jsonb_minus_setup.sql
175
+ - bench/keys2_trigger_setup.sql
101
176
  - bench/keys_trigger_setup.sql
177
+ - bin/console
178
+ - bin/setup
179
+ - circle.yml
180
+ - gemfiles/rails42.gemfile
181
+ - gemfiles/rails5.gemfile
182
+ - lib/generators/logidze/install/USAGE
183
+ - lib/generators/logidze/install/install_generator.rb
184
+ - lib/generators/logidze/install/templates/hstore.rb.erb
185
+ - lib/generators/logidze/install/templates/migration.rb.erb
186
+ - lib/generators/logidze/model/USAGE
187
+ - lib/generators/logidze/model/model_generator.rb
188
+ - lib/generators/logidze/model/templates/migration.rb.erb
102
189
  - lib/logidze.rb
190
+ - lib/logidze/engine.rb
191
+ - lib/logidze/has_logidze.rb
192
+ - lib/logidze/history.rb
193
+ - lib/logidze/model.rb
103
194
  - lib/logidze/version.rb
104
195
  - logidze.gemspec
105
196
  homepage: http://github.com/palkan/logidze
@@ -122,9 +213,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
213
  version: '0'
123
214
  requirements: []
124
215
  rubyforge_project:
125
- rubygems_version: 2.5.1
216
+ rubygems_version: 2.6.4
126
217
  signing_key:
127
218
  specification_version: 4
128
219
  summary: PostgreSQL JSON-based auditing
129
220
  test_files: []
130
- has_rdoc: