logidze 0.0.1 → 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/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: