recorder 0.1.23 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop.yml +27 -0
- data/Gemfile +2 -0
- data/README.md +33 -3
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/lib/generators/recorder/install_generator.rb +14 -12
- data/lib/generators/recorder/templates/add_index_by_user_id_to_recorder_revisions.rb +2 -0
- data/lib/generators/recorder/templates/add_number_column_to_recorder_revisions.rb +23 -21
- data/lib/generators/recorder/templates/create_recorder_revisions.rb +4 -2
- data/lib/recorder/changeset.rb +14 -12
- data/lib/recorder/config.rb +13 -5
- data/lib/recorder/manager.rb +18 -0
- data/lib/recorder/observer.rb +11 -18
- data/lib/recorder/rails/controller_concern.rb +10 -7
- data/lib/recorder/rails/railtie.rb +8 -4
- data/lib/recorder/revision.rb +75 -54
- data/lib/recorder/sidekiq/revisions_worker.rb +4 -3
- data/lib/recorder/store.rb +36 -0
- data/lib/recorder/tape/data.rb +83 -0
- data/lib/recorder/tape/record.rb +39 -0
- data/lib/recorder/tape.rb +32 -79
- data/lib/recorder/version.rb +3 -13
- data/lib/recorder.rb +14 -10
- data/recorder.gemspec +24 -23
- metadata +40 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4af26e2ed8bedd59ea727cc40967738a53967495d7109401f3698895e4cc394e
|
4
|
+
data.tar.gz: 356249bc613ab922c13deb3d9b8f91fd82fd99984b3e44e258b4d04720ab49c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 27b9d07aa2667fefb5d6f5f2acba706a1397da9c37af28bcf8f775753bf183507acbd4a5cf6604c16e5b7017b34a7b1c430d13db0407144038d46807a6ced3aa
|
7
|
+
data.tar.gz: 456c8e4ccffd562f766a732940b1c0378806bd71b18b223de6d3926ef0fafc956856d1bfbdde93ff9287857a5908bc1ac8715218dc88d5fed5b15b3ac6c9c5dc
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
jetrockets-standard: config/gems.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
Exclude:
|
6
|
+
- 'bin/*'
|
7
|
+
- 'tmp/**/*'
|
8
|
+
- 'docs/**/*'
|
9
|
+
- 'Gemfile'
|
10
|
+
- 'vendor/**/*'
|
11
|
+
- 'gemfiles/**/*'
|
12
|
+
DisplayCopNames: true
|
13
|
+
TargetRubyVersion: 2.5
|
14
|
+
|
15
|
+
Style/TrailingCommaInArrayLiteral:
|
16
|
+
EnforcedStyleForMultiline: no_comma
|
17
|
+
|
18
|
+
Style/TrailingCommaInHashLiteral:
|
19
|
+
EnforcedStyleForMultiline: no_comma
|
20
|
+
|
21
|
+
Layout/ParameterAlignment:
|
22
|
+
EnforcedStyle: with_first_parameter
|
23
|
+
|
24
|
+
# See https://github.com/rubocop-hq/rubocop/issues/4222
|
25
|
+
Lint/AmbiguousBlockAssociation:
|
26
|
+
Exclude:
|
27
|
+
- 'spec/**/*'
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -20,7 +20,37 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
|
23
|
+
To enable logging on a model you just need to include `Recorder::Observer` into the model and configure logging options for it:
|
24
|
+
|
25
|
+
``` ruby
|
26
|
+
class Post < ActiveRecord::Base
|
27
|
+
include ::Recorder::Observer
|
28
|
+
|
29
|
+
recorder only: %i[title tags],
|
30
|
+
associations: {
|
31
|
+
author: { only: %i[full_name] },
|
32
|
+
category: { only: %i[name slug] }
|
33
|
+
}
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Recorder supports the following options:
|
38
|
+
|
39
|
+
* `ignore: [array]` - attributes that are ignored on logging;
|
40
|
+
* `only: [array]` - only these attributes are logged, other attributes are ingored;
|
41
|
+
* `associations: {hash} (hash)` - allows to set what associations will be logged alongside with the model. For each association you can also set ignore and only options;
|
42
|
+
* `async: bool` - a logging strategy (true - asynchronous, false - synchronous).
|
43
|
+
|
44
|
+
There are two strategies for logging: synchronous and asynchronous. When the synchronous strategy is used, a revision record is saved immediately after a model is saved, and the async strategy moves creating of revision records to [Sidekiq](https://github.com/mperham/sidekiq).
|
45
|
+
|
46
|
+
To enable storing of such data as user_id and ip, you need to include `Recorder::Rails::ControllerConcern` to `ApplicationController`. Recorder uses [request_store](https://github.com/steveklabnik/request_store) to safely store these data on a thread level.
|
47
|
+
|
48
|
+
``` ruby
|
49
|
+
class ApplicationController < ActionController::Base
|
50
|
+
include Recorder::Rails::ControllerConcern
|
51
|
+
...
|
52
|
+
end
|
53
|
+
```
|
24
54
|
|
25
55
|
## Development
|
26
56
|
|
@@ -34,9 +64,9 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jetroc
|
|
34
64
|
|
35
65
|
## Credits
|
36
66
|
|
37
|
-
![JetRockets](
|
67
|
+
![JetRockets](https://media.jetrockets.pro/jetrockets-white.png)
|
38
68
|
|
39
|
-
Recorder is maintained by [JetRockets](
|
69
|
+
Recorder is maintained by [JetRockets](https://www.jetrockets.pro]).
|
40
70
|
|
41
71
|
## License
|
42
72
|
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'recorder'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "recorder"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start
|
@@ -1,48 +1,50 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
3
5
|
|
4
6
|
module Recorder
|
5
7
|
# Installs Recorder in a rails app.
|
6
8
|
class InstallGenerator < ::Rails::Generators::Base
|
7
9
|
include ::Rails::Generators::Migration
|
8
10
|
|
9
|
-
source_root File.expand_path(
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
10
12
|
|
11
13
|
class_option(
|
12
14
|
:with_partitions,
|
13
15
|
type: :boolean,
|
14
16
|
default: false,
|
15
|
-
desc:
|
17
|
+
desc: 'Create partitions to `recorder_revisions` table'
|
16
18
|
)
|
17
19
|
|
18
20
|
class_option(
|
19
21
|
:with_number_column,
|
20
22
|
type: :boolean,
|
21
23
|
default: false,
|
22
|
-
desc:
|
24
|
+
desc: 'Add `number` column to `recorder_revisions` table'
|
23
25
|
)
|
24
26
|
|
25
27
|
class_option(
|
26
28
|
:with_index_by_user_id,
|
27
29
|
type: :boolean,
|
28
30
|
default: false,
|
29
|
-
desc:
|
31
|
+
desc: 'Add index by `user_id` column to `recorder_revisions` table'
|
30
32
|
)
|
31
33
|
|
32
|
-
desc
|
34
|
+
desc 'Generates (but does not run) a migration to add a `recorder_revisions` table.'
|
33
35
|
|
34
36
|
def create_migration_file
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
add_or_skip_recorder_migration('create_recorder_revisions')
|
38
|
+
add_or_skip_recorder_migration('add_number_column_to_recorder_revisions') if options.with_number_column?
|
39
|
+
add_or_skip_recorder_migration('add_index_by_user_id_to_recorder_revisions') if options.with_index_by_user_id?
|
40
|
+
add_or_skip_recorder_migration('add_partitions_to_recorder_revisions') if options.with_partitions?
|
39
41
|
end
|
40
42
|
|
41
43
|
def self.next_migration_number(dirname)
|
42
44
|
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
43
45
|
end
|
44
46
|
|
45
|
-
|
47
|
+
protected
|
46
48
|
|
47
49
|
def add_or_skip_recorder_migration(template)
|
48
50
|
migration_dir = File.expand_path('db/migrate')
|
@@ -1,30 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This migration adds number column to the `revisions` table.
|
2
4
|
class AddNumberColumnToRecorderRevisions < ActiveRecord::Migration
|
3
5
|
def up
|
4
|
-
add_column :recorder_revisions, :number, :integer, :
|
5
|
-
|
6
|
-
execute <<-SQL
|
7
|
-
CREATE OR REPLACE FUNCTION get_recorder_revisions_number()
|
8
|
-
RETURNS trigger AS
|
9
|
-
$BODY$
|
10
|
-
BEGIN
|
11
|
-
SELECT COALESCE(MAX(recorder_revisions.number), 0) + 1
|
12
|
-
INTO NEW.number
|
13
|
-
FROM
|
14
|
-
recorder_revisions
|
15
|
-
WHERE
|
16
|
-
recorder_revisions.item_type = NEW.item_type
|
17
|
-
AND recorder_revisions.item_id = NEW.item_id;
|
6
|
+
add_column :recorder_revisions, :number, :integer, null: false, default: 0
|
18
7
|
|
19
|
-
|
20
|
-
|
21
|
-
|
8
|
+
execute <<~SQL
|
9
|
+
CREATE OR REPLACE FUNCTION get_recorder_revisions_number()
|
10
|
+
RETURNS trigger AS
|
11
|
+
$BODY$
|
12
|
+
BEGIN
|
13
|
+
SELECT COALESCE(MAX(recorder_revisions.number), 0) + 1
|
14
|
+
INTO NEW.number
|
15
|
+
FROM
|
16
|
+
recorder_revisions
|
17
|
+
WHERE
|
18
|
+
recorder_revisions.item_type = NEW.item_type
|
19
|
+
AND recorder_revisions.item_id = NEW.item_id;
|
20
|
+
#{" "}
|
21
|
+
RETURN NEW;
|
22
|
+
END;
|
23
|
+
$BODY$ LANGUAGE plpgsql;
|
22
24
|
SQL
|
23
25
|
|
24
|
-
execute
|
25
|
-
CREATE TRIGGER update_recorder_revisions_number
|
26
|
-
|
27
|
-
|
26
|
+
execute <<~SQL
|
27
|
+
CREATE TRIGGER update_recorder_revisions_number
|
28
|
+
BEFORE INSERT ON recorder_revisions FOR EACH ROW
|
29
|
+
EXECUTE PROCEDURE get_recorder_revisions_number();
|
28
30
|
SQL
|
29
31
|
end
|
30
32
|
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This migration creates the `recorder_revisions` table.
|
2
4
|
class CreateRecorderRevisions < ActiveRecord::Migration
|
3
5
|
def change
|
4
6
|
create_table :recorder_revisions do |t|
|
5
7
|
t.string :item_type, null: false
|
6
|
-
t.integer :item_id
|
8
|
+
t.integer :item_id
|
7
9
|
t.string :event, null: false
|
8
10
|
t.jsonb :data, null: false
|
9
11
|
t.inet :ip
|
@@ -13,6 +15,6 @@ class CreateRecorderRevisions < ActiveRecord::Migration
|
|
13
15
|
t.datetime :created_at, null: false
|
14
16
|
end
|
15
17
|
|
16
|
-
add_index :recorder_revisions, [
|
18
|
+
add_index :recorder_revisions, %i[item_type item_id]
|
17
19
|
end
|
18
20
|
end
|
data/lib/recorder/changeset.rb
CHANGED
@@ -1,34 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Recorder
|
2
4
|
class Changeset
|
3
5
|
attr_reader :item, :changes
|
4
6
|
|
5
7
|
def initialize(item, changes)
|
6
|
-
@item = item
|
7
|
-
@changes =
|
8
|
+
@item = item
|
9
|
+
@changes = changes.to_h
|
8
10
|
end
|
9
11
|
|
10
12
|
def keys
|
11
|
-
|
13
|
+
changes.try(:keys) || []
|
12
14
|
end
|
13
15
|
|
14
16
|
def human_attribute_name(attribute)
|
15
|
-
if defined?(Draper) &&
|
16
|
-
|
17
|
+
if defined?(Draper) && item.decorated?
|
18
|
+
item.source.class.human_attribute_name(attribute.to_s)
|
17
19
|
else
|
18
|
-
|
20
|
+
item.class.human_attribute_name(attribute.to_s)
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
24
|
def previous(attribute)
|
23
|
-
|
25
|
+
try("previous_#{attribute}") || previous_version.try(attribute)
|
24
26
|
end
|
25
27
|
|
26
28
|
def previous_version
|
27
29
|
return @previous_version if defined?(@previous_version)
|
28
30
|
|
29
|
-
@previous_version =
|
31
|
+
@previous_version = item.dup
|
30
32
|
|
31
|
-
|
33
|
+
changes.each do |key, change|
|
32
34
|
@previous_version.send("#{key}=", change[0])
|
33
35
|
end
|
34
36
|
|
@@ -36,15 +38,15 @@ module Recorder
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def next(attribute)
|
39
|
-
|
41
|
+
try("next_#{attribute}") || next_version.try(attribute)
|
40
42
|
end
|
41
43
|
|
42
44
|
def next_version
|
43
45
|
return @next_version if defined?(@next_version)
|
44
46
|
|
45
|
-
@next_version =
|
47
|
+
@next_version = item.dup
|
46
48
|
|
47
|
-
|
49
|
+
changes.each do |key, change|
|
48
50
|
@next_version.send("#{key}=", change[1])
|
49
51
|
end
|
50
52
|
|
data/lib/recorder/config.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'singleton'
|
2
4
|
|
3
5
|
module Recorder
|
4
6
|
# Global configuration options
|
5
7
|
class Config
|
6
8
|
include Singleton
|
7
|
-
attr_accessor :sidekiq_options
|
9
|
+
attr_accessor :sidekiq_options
|
10
|
+
attr_reader :ignore, :async
|
8
11
|
|
9
12
|
def initialize
|
10
13
|
# Variables which affect all threads, whose access is synchronized.
|
@@ -12,18 +15,23 @@ module Recorder
|
|
12
15
|
@enabled = true
|
13
16
|
|
14
17
|
@sidekiq_options = {
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
+
queue: 'recorder',
|
19
|
+
retry: 10,
|
20
|
+
backtrace: true
|
18
21
|
}
|
19
22
|
|
20
|
-
@ignore =
|
23
|
+
@ignore = []
|
24
|
+
@async = false
|
21
25
|
end
|
22
26
|
|
23
27
|
def ignore=(value)
|
24
28
|
@ignore = Array.wrap(value).map(&:to_sym)
|
25
29
|
end
|
26
30
|
|
31
|
+
def async=(value)
|
32
|
+
@async = !!value
|
33
|
+
end
|
34
|
+
|
27
35
|
# Indicates whether Recorder is on or off. Default: true.
|
28
36
|
def enabled
|
29
37
|
@mutex.synchronize { !!@enabled }
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Recorder
|
4
|
+
module Manager
|
5
|
+
def recorder_disabled!
|
6
|
+
Recorder.store.recorder_disabled!
|
7
|
+
|
8
|
+
if block_given?
|
9
|
+
yield
|
10
|
+
Recorder.store.recorder_enabled!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def recorder_enabled!
|
15
|
+
Recorder.store.recorder_enabled!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/recorder/observer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'recorder/tape'
|
2
4
|
require 'active_support/concern'
|
3
5
|
|
@@ -6,22 +8,19 @@ module Recorder
|
|
6
8
|
extend ::ActiveSupport::Concern
|
7
9
|
|
8
10
|
included do
|
9
|
-
has_many :revisions, :
|
10
|
-
def create_async(params)
|
11
|
-
Recorder::Sidekiq::RevisionsWorker.perform_async(
|
12
|
-
proxy_association.owner.class.to_s,
|
13
|
-
proxy_association.owner.id,
|
14
|
-
params
|
15
|
-
)
|
16
|
-
end
|
17
|
-
end
|
11
|
+
has_many :revisions, class_name: '::Recorder::Revision', inverse_of: :item, as: :item
|
18
12
|
end
|
19
13
|
|
20
14
|
def recorder_dirty?
|
21
15
|
return @recorder_dirty if defined?(@recorder_dirty)
|
16
|
+
|
22
17
|
true
|
23
18
|
end
|
24
19
|
|
20
|
+
def recorder_record?
|
21
|
+
recorder_dirty? && Recorder.store.recorder_enabled?
|
22
|
+
end
|
23
|
+
|
25
24
|
module ClassMethods
|
26
25
|
def recorder(options = {})
|
27
26
|
define_method 'recorder_options' do
|
@@ -29,21 +28,15 @@ module Recorder
|
|
29
28
|
end
|
30
29
|
|
31
30
|
after_create do
|
32
|
-
if
|
33
|
-
Recorder::Tape.new(self).record_create
|
34
|
-
end
|
31
|
+
Recorder::Tape.new(self).record_create if recorder_record?
|
35
32
|
end
|
36
33
|
|
37
34
|
after_update do
|
38
|
-
if
|
39
|
-
Recorder::Tape.new(self).record_update
|
40
|
-
end
|
35
|
+
Recorder::Tape.new(self).record_update if recorder_record?
|
41
36
|
end
|
42
37
|
|
43
38
|
after_destroy do
|
44
|
-
if
|
45
|
-
Recorder::Tape.new(self).record_destroy
|
46
|
-
end
|
39
|
+
Recorder::Tape.new(self).record_destroy if recorder_record?
|
47
40
|
end
|
48
41
|
end
|
49
42
|
end
|