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