logidze 0.2.3 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +55 -1
- data/lib/generators/logidze/install/install_generator.rb +23 -1
- data/lib/generators/logidze/install/templates/migration.rb.erb +32 -24
- data/lib/generators/logidze/model/model_generator.rb +8 -1
- data/lib/generators/logidze/model/templates/migration.rb.erb +6 -2
- data/lib/logidze.rb +3 -0
- data/lib/logidze/history.rb +5 -0
- data/lib/logidze/history/version.rb +6 -0
- data/lib/logidze/model.rb +10 -6
- data/lib/logidze/responsible.rb +17 -0
- data/lib/logidze/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 937e0b13769d464f7bb14dc1440951ea6a1462db
|
4
|
+
data.tar.gz: e7d117211dc9e24854b0171158996066aa56ade1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcdfe2828fa0fa360177a7839de761bc91b7e9bb81e9ce0d69482e28b3d2c1194a91e71dca9e13975d7eafc31839db3990f7b082fa37f132c43b51bad3245010
|
7
|
+
data.tar.gz: 3fd4562dd287fdcf8c1321e5ff8e225931cbc9b76913f1c90f9a4566f2610446f86febddfed60407988746a4ad765d397d469bef75eb06591938b2978198866c
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 0.3.0
|
2
|
+
- Add `--update` option to install migration generator
|
3
|
+
- Add `--only-trigger` option to model migration generator
|
4
|
+
- Add [Responsibility](https://github.com/palkan/logidze/issues/4) feature
|
5
|
+
|
6
|
+
# 0.2.3
|
7
|
+
- Support Ruby >= 2.1
|
8
|
+
|
1
9
|
# 0.2.2
|
2
10
|
- Add `--backfill` option to model migration
|
3
11
|
- Handle legacy data (that doesn't have log data)
|
data/README.md
CHANGED
@@ -60,6 +60,16 @@ To backfill table data (i.e. create initial snapshots) add `backfill` option:
|
|
60
60
|
rails generate logidze:model Post --backfill
|
61
61
|
```
|
62
62
|
|
63
|
+
## Upgrade from previous versions
|
64
|
+
|
65
|
+
We try to make upgrade process as simple as possible. For now, the only required action is to create and run a migration:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
rails generate logidze:install --update
|
69
|
+
```
|
70
|
+
|
71
|
+
This updates core `logdize_logger` DB function. No need to update tables or triggers.
|
72
|
+
|
63
73
|
## Usage
|
64
74
|
|
65
75
|
Your model now has `log_data` column which stores changes log.
|
@@ -122,6 +132,49 @@ post.switch_to!(2)
|
|
122
132
|
|
123
133
|
If you update record after `#undo!` or `#switch_to!` you lose all "future" versions and `#redo!` is no longer possible.
|
124
134
|
|
135
|
+
## Track responsibility (aka _whodunnit_)
|
136
|
+
|
137
|
+
You can store additional information in the version object, which is called _Responsible ID_. There is more likely that you would like to store the `current_user.id` that way.
|
138
|
+
|
139
|
+
To provide `responsible_id` you should wrap your code in a block:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
Logidze.with_responsible(user.id) do
|
143
|
+
post.save!
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
And then to retrieve `responsible_id`:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
post.log_data.responsible_id
|
151
|
+
```
|
152
|
+
|
153
|
+
Logidze does not require `responsible_id` to be `SomeModel` ID. It can be anything. Thus Logidze does not provide methods for retrieving the corresponding object. However, you can easy write it yourself:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class Post < ActiveRecord::Base
|
157
|
+
has_logidze
|
158
|
+
|
159
|
+
def whodunnit
|
160
|
+
id = log_data.responsible_id
|
161
|
+
User.find(id) if id.present?
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
And in your controller:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class ApplicationController < ActionController::Base
|
170
|
+
around_action :set_logidze_responsible, only: [:create, :update]
|
171
|
+
|
172
|
+
def set_logidze_responsible(&block)
|
173
|
+
Logidze.with_responsible(current_user&.id, &block)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
125
178
|
## Disable logging temporary
|
126
179
|
|
127
180
|
If you want to make update without logging (e.g. mass update), you can turn it off the following way:
|
@@ -150,7 +203,8 @@ The `log_data` column has the following format:
|
|
150
203
|
"attr": "new value", // updated fields with new values
|
151
204
|
"attr2": "new value"
|
152
205
|
}
|
153
|
-
}
|
206
|
+
},
|
207
|
+
"r": 42 // Resposibility ID (if provided)
|
154
208
|
]
|
155
209
|
}
|
156
210
|
```
|
@@ -9,14 +9,36 @@ module Logidze
|
|
9
9
|
|
10
10
|
source_root File.expand_path('../templates', __FILE__)
|
11
11
|
|
12
|
+
class_option :update, type: :boolean, optional: true,
|
13
|
+
desc: "Define whether this is an update migration"
|
14
|
+
|
12
15
|
def generate_migration
|
13
|
-
migration_template "migration.rb.erb", "db/migrate
|
16
|
+
migration_template "migration.rb.erb", "db/migrate/#{migration_name}.rb"
|
14
17
|
end
|
15
18
|
|
16
19
|
def generate_hstore_migration
|
20
|
+
return if update?
|
17
21
|
migration_template "hstore.rb.erb", "db/migrate/enable_hstore.rb"
|
18
22
|
end
|
19
23
|
|
24
|
+
no_tasks do
|
25
|
+
def migration_name
|
26
|
+
if update?
|
27
|
+
"logidze_update_#{Logidze::VERSION.delete('.')}"
|
28
|
+
else
|
29
|
+
"logidze_install"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def migration_class_name
|
34
|
+
migration_name.classify
|
35
|
+
end
|
36
|
+
|
37
|
+
def update?
|
38
|
+
options[:update]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
20
42
|
def self.next_migration_number(dir)
|
21
43
|
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
22
44
|
end
|
@@ -4,25 +4,39 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
4
4
|
DO $$
|
5
5
|
BEGIN
|
6
6
|
EXECUTE 'ALTER DATABASE ' || current_database() || ' SET logidze.disabled TO off';
|
7
|
+
EXECUTE 'ALTER DATABASE ' || current_database() || ' SET logidze.responsible TO off';
|
7
8
|
END;
|
8
9
|
$$
|
9
10
|
LANGUAGE plpgsql;
|
10
11
|
SQL
|
11
12
|
|
12
13
|
execute <<-SQL
|
14
|
+
CREATE OR REPLACE FUNCTION logidze_version(v bigint, data jsonb) RETURNS jsonb AS $body$
|
15
|
+
DECLARE
|
16
|
+
buf jsonb;
|
17
|
+
BEGIN
|
18
|
+
buf := jsonb_build_object(
|
19
|
+
'ts',
|
20
|
+
(extract(epoch from now()) * 1000)::bigint,
|
21
|
+
'v',
|
22
|
+
v,
|
23
|
+
'c',
|
24
|
+
logidze_exclude_keys(data, 'log_data')
|
25
|
+
);
|
26
|
+
IF current_setting('logidze.responsible') <> 'off' THEN
|
27
|
+
buf := jsonb_set(buf, ARRAY['r'], to_jsonb(current_setting('logidze.responsible')));
|
28
|
+
END IF;
|
29
|
+
RETURN buf;
|
30
|
+
END;
|
31
|
+
$body$
|
32
|
+
LANGUAGE plpgsql;
|
33
|
+
|
13
34
|
CREATE OR REPLACE FUNCTION logidze_snapshot(item jsonb) RETURNS jsonb AS $body$
|
14
35
|
BEGIN
|
15
36
|
return json_build_object(
|
16
37
|
'v', 1,
|
17
38
|
'h', jsonb_build_array(
|
18
|
-
|
19
|
-
'ts',
|
20
|
-
(extract(epoch from now()) * 1000)::bigint,
|
21
|
-
'v',
|
22
|
-
1,
|
23
|
-
'c',
|
24
|
-
logidze_exclude_keys(item,'log_data')
|
25
|
-
)
|
39
|
+
logidze_version(1, item)
|
26
40
|
)
|
27
41
|
);
|
28
42
|
END;
|
@@ -57,6 +71,10 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
57
71
|
(log_data#>'{h,0,c}') || (log_data#>'{h,1,c}')
|
58
72
|
);
|
59
73
|
|
74
|
+
IF (log_data#>'{h,1}' ? 'r') THEN
|
75
|
+
merged := jsonb_set(merged, ARRAY['r'], log_data#>'{h,1,r}');
|
76
|
+
END IF;
|
77
|
+
|
60
78
|
return jsonb_set(
|
61
79
|
log_data,
|
62
80
|
'{h}',
|
@@ -74,7 +92,6 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
74
92
|
DECLARE
|
75
93
|
changes jsonb;
|
76
94
|
new_v integer;
|
77
|
-
ts bigint;
|
78
95
|
size integer;
|
79
96
|
history_limit integer;
|
80
97
|
current_version integer;
|
@@ -101,8 +118,6 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
101
118
|
RETURN NEW;
|
102
119
|
END IF;
|
103
120
|
|
104
|
-
ts := (extract(epoch from now()) * 1000)::bigint;
|
105
|
-
|
106
121
|
IF current_version < (NEW.log_data#>>'{h,-1,v}')::int THEN
|
107
122
|
iterator := 0;
|
108
123
|
FOR item in SELECT * FROM jsonb_array_elements(NEW.log_data->'h')
|
@@ -118,11 +133,8 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
118
133
|
END LOOP;
|
119
134
|
END IF;
|
120
135
|
|
121
|
-
changes :=
|
122
|
-
|
123
|
-
hstore(NEW.*) - hstore(OLD.*)
|
124
|
-
),
|
125
|
-
'log_data'
|
136
|
+
changes := hstore_to_jsonb_loose(
|
137
|
+
hstore(NEW.*) - hstore(OLD.*)
|
126
138
|
);
|
127
139
|
|
128
140
|
new_v := (NEW.log_data#>>'{h,-1,v}')::int + 1;
|
@@ -132,14 +144,7 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
132
144
|
NEW.log_data := jsonb_set(
|
133
145
|
NEW.log_data,
|
134
146
|
ARRAY['h', size::text],
|
135
|
-
|
136
|
-
'ts',
|
137
|
-
ts,
|
138
|
-
'v',
|
139
|
-
new_v,
|
140
|
-
'c',
|
141
|
-
changes
|
142
|
-
),
|
147
|
+
logidze_version(new_v, changes),
|
143
148
|
true
|
144
149
|
);
|
145
150
|
|
@@ -162,10 +167,13 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
162
167
|
end
|
163
168
|
|
164
169
|
def down
|
170
|
+
<% unless update? %>
|
165
171
|
execute <<-SQL
|
172
|
+
DROP FUNCTION logidze_version(bigint, jsonb) CASCADE;
|
166
173
|
DROP FUNCTION logidze_compact_history(jsonb) CASCADE;
|
167
174
|
DROP FUNCTION logidze_snapshot(jsonb) CASCADE;
|
168
175
|
DROP FUNCTION logidze_logger() CASCADE;
|
169
176
|
SQL
|
177
|
+
<% end %>
|
170
178
|
end
|
171
179
|
end
|
@@ -12,6 +12,9 @@ module Logidze
|
|
12
12
|
class_option :backfill, type: :boolean, optional: true,
|
13
13
|
desc: "Add query to backfill existing records history"
|
14
14
|
|
15
|
+
class_option :only_trigger, type: :boolean, optional: true,
|
16
|
+
desc: "Create trigger-only migration"
|
17
|
+
|
15
18
|
def generate_migration
|
16
19
|
migration_template "migration.rb.erb", "db/migrate/#{migration_file_name}"
|
17
20
|
end
|
@@ -35,9 +38,13 @@ module Logidze
|
|
35
38
|
options[:limit]
|
36
39
|
end
|
37
40
|
|
38
|
-
def backfill
|
41
|
+
def backfill?
|
39
42
|
options[:backfill]
|
40
43
|
end
|
44
|
+
|
45
|
+
def only_trigger?
|
46
|
+
options[:only_trigger]
|
47
|
+
end
|
41
48
|
end
|
42
49
|
|
43
50
|
private
|
@@ -1,6 +1,8 @@
|
|
1
1
|
class <%= @migration_class_name %> < ActiveRecord::Migration
|
2
2
|
def up
|
3
|
-
|
3
|
+
<% unless only_trigger? %>
|
4
|
+
add_column :<%= table_name %>, :log_data, :jsonb
|
5
|
+
<% end %>
|
4
6
|
|
5
7
|
execute <<-SQL
|
6
8
|
CREATE TRIGGER logidze_on_<%= table_name %>
|
@@ -9,7 +11,7 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
9
11
|
EXECUTE PROCEDURE logidze_logger(<%= limit || '' %>);
|
10
12
|
SQL
|
11
13
|
|
12
|
-
<% if backfill %>
|
14
|
+
<% if backfill? %>
|
13
15
|
execute <<-SQL
|
14
16
|
UPDATE <%= table_name %> as t
|
15
17
|
SET log_data = logidze_snapshot(to_jsonb(t));
|
@@ -20,6 +22,8 @@ class <%= @migration_class_name %> < ActiveRecord::Migration
|
|
20
22
|
def down
|
21
23
|
execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
|
22
24
|
|
25
|
+
<% unless only_trigger? %>
|
23
26
|
remove_column :<%= table_name %>, :log_data
|
27
|
+
<% end %>
|
24
28
|
end
|
25
29
|
end
|
data/lib/logidze.rb
CHANGED
data/lib/logidze/history.rb
CHANGED
@@ -12,6 +12,7 @@ module Logidze
|
|
12
12
|
attr_reader :data
|
13
13
|
|
14
14
|
delegate :size, to: :versions
|
15
|
+
delegate :responsible_id, to: :current_version
|
15
16
|
|
16
17
|
### Rails 4 ###
|
17
18
|
def self.dump(object)
|
@@ -105,6 +106,10 @@ module Logidze
|
|
105
106
|
versions.reverse.find { |v| v.time <= time }
|
106
107
|
end
|
107
108
|
|
109
|
+
def dup
|
110
|
+
self.class.new(data.deep_dup)
|
111
|
+
end
|
112
|
+
|
108
113
|
def ==(other)
|
109
114
|
return super unless other.is_a?(self.class)
|
110
115
|
data == other.data
|
@@ -7,6 +7,8 @@ module Logidze
|
|
7
7
|
TS = 'ts'
|
8
8
|
# Changes key
|
9
9
|
CHANGES = 'c'
|
10
|
+
# Responsible ID
|
11
|
+
RESPONSIBLE = 'r'
|
10
12
|
|
11
13
|
attr_reader :data
|
12
14
|
|
@@ -25,6 +27,10 @@ module Logidze
|
|
25
27
|
def time
|
26
28
|
data.fetch(TS)
|
27
29
|
end
|
30
|
+
|
31
|
+
def responsible_id
|
32
|
+
data[RESPONSIBLE]
|
33
|
+
end
|
28
34
|
end
|
29
35
|
end
|
30
36
|
end
|
data/lib/logidze/model.rb
CHANGED
@@ -46,8 +46,10 @@ module Logidze
|
|
46
46
|
return nil unless log_data.exists_ts?(ts)
|
47
47
|
return self if log_data.current_ts?(ts)
|
48
48
|
|
49
|
+
version = log_data.find_by_time(ts).version
|
50
|
+
|
49
51
|
object_at = dup
|
50
|
-
object_at.apply_diff(log_data.changes_to(
|
52
|
+
object_at.apply_diff(version, log_data.changes_to(version: version))
|
51
53
|
end
|
52
54
|
|
53
55
|
# Revert record to the version at specified time (without saving to DB)
|
@@ -56,7 +58,9 @@ module Logidze
|
|
56
58
|
return self if log_data.current_ts?(ts)
|
57
59
|
return false unless log_data.exists_ts?(ts)
|
58
60
|
|
59
|
-
|
61
|
+
version = log_data.find_by_time(ts).version
|
62
|
+
|
63
|
+
apply_diff(version, log_data.changes_to(version: version))
|
60
64
|
end
|
61
65
|
|
62
66
|
# Return a dirty copy of specified version of record
|
@@ -65,7 +69,7 @@ module Logidze
|
|
65
69
|
return nil unless log_data.find_by_version(version)
|
66
70
|
|
67
71
|
object_at = dup
|
68
|
-
object_at.apply_diff(log_data.changes_to(version: version))
|
72
|
+
object_at.apply_diff(version, log_data.changes_to(version: version))
|
69
73
|
end
|
70
74
|
|
71
75
|
# Revert record to the specified version (without saving to DB)
|
@@ -73,7 +77,7 @@ module Logidze
|
|
73
77
|
return self if log_data.version == version
|
74
78
|
return false unless log_data.find_by_version(version)
|
75
79
|
|
76
|
-
apply_diff(log_data.changes_to(version: version))
|
80
|
+
apply_diff(version, log_data.changes_to(version: version))
|
77
81
|
end
|
78
82
|
|
79
83
|
# Return diff object representing changes since specified time.
|
@@ -107,14 +111,14 @@ module Logidze
|
|
107
111
|
# Return false if version is unknown.
|
108
112
|
def switch_to!(version)
|
109
113
|
return false unless at_version!(version)
|
110
|
-
log_data.version = version
|
111
114
|
self.class.without_logging { save! }
|
112
115
|
end
|
113
116
|
|
114
117
|
protected
|
115
118
|
|
116
|
-
def apply_diff(diff)
|
119
|
+
def apply_diff(version, diff)
|
117
120
|
diff.each { |k, v| send("#{k}=", v) }
|
121
|
+
log_data.version = version
|
118
122
|
self
|
119
123
|
end
|
120
124
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Logidze # :nodoc:
|
3
|
+
# Provide methods to work with "responsibility" feature
|
4
|
+
module Responsible
|
5
|
+
def with_responsible(responsible_id)
|
6
|
+
return yield if responsible_id.nil?
|
7
|
+
ActiveRecord::Base.transaction do
|
8
|
+
ActiveRecord::Base.connection.execute(
|
9
|
+
"SET LOCAL logidze.responsible = #{ActiveRecord::Base.connection.quote(responsible_id)};"
|
10
|
+
)
|
11
|
+
res = yield
|
12
|
+
ActiveRecord::Base.connection.execute "SET LOCAL logidze.responsible = DEFAULT;"
|
13
|
+
res
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/logidze/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logidze
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.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-07-
|
11
|
+
date: 2016-07-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -186,6 +186,7 @@ files:
|
|
186
186
|
- lib/logidze/history/type.rb
|
187
187
|
- lib/logidze/history/version.rb
|
188
188
|
- lib/logidze/model.rb
|
189
|
+
- lib/logidze/responsible.rb
|
189
190
|
- lib/logidze/version.rb
|
190
191
|
- logidze.gemspec
|
191
192
|
homepage: http://github.com/palkan/logidze
|