recorder 0.1.20 → 1.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.
- checksums.yaml +5 -5
- 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 -2
- 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 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
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
|
-

|
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
|