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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f91810e96c04d3ddc2d629c5aa0179c69a232d52
4
- data.tar.gz: 633e81904255a8d8186dc3839c59bb8c84623276
3
+ metadata.gz: 937e0b13769d464f7bb14dc1440951ea6a1462db
4
+ data.tar.gz: e7d117211dc9e24854b0171158996066aa56ade1
5
5
  SHA512:
6
- metadata.gz: 039e4e7543e6a9e5415eccd73921fff6a850a25e11b68b437d25356ede09f7c89e713803fdad7c77d8687d22a5ce2fae2809a6765d435f1ffc415c85e6ca33dc
7
- data.tar.gz: 0db11276109aa07f6b2350e2acd4034b6cfe4f76325ba97f16ea14306c949fca0362520246454ad65e1becad13411d58ea5d7c7e6c09dd233dceca680a9a7453
6
+ metadata.gz: fcdfe2828fa0fa360177a7839de761bc91b7e9bb81e9ce0d69482e28b3d2c1194a91e71dca9e13975d7eafc31839db3990f7b082fa37f132c43b51bad3245010
7
+ data.tar.gz: 3fd4562dd287fdcf8c1321e5ff8e225931cbc9b76913f1c90f9a4566f2610446f86febddfed60407988746a4ad765d397d469bef75eb06591938b2978198866c
@@ -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/logidze_install.rb"
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
- jsonb_build_object(
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 := logidze_exclude_keys(
122
- hstore_to_jsonb_loose(
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
- jsonb_build_object(
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
- add_column :<%= table_name %>, :log_data, :jsonb, default: {}, null: false
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
@@ -7,6 +7,9 @@ module Logidze
7
7
  require 'logidze/history'
8
8
  require 'logidze/model'
9
9
  require 'logidze/has_logidze'
10
+ require 'logidze/responsible'
11
+
12
+ extend Logidze::Responsible
10
13
 
11
14
  require 'logidze/engine' if defined?(Rails)
12
15
 
@@ -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
@@ -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(time: ts))
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
- apply_diff(log_data.changes_to(time: ts))
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Logidze
3
- VERSION = "0.2.3"
3
+ VERSION = "0.3.0"
4
4
  end
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.2.3
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-09 00:00:00.000000000 Z
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