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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ca2c4ebb8079516d7eb4720436938dbb781dbf93d4ac9f970b3245cb04601af
4
- data.tar.gz: 39e4b51785486dfb7e865561a37a503271d6ad34430e6cb1096ef6d2fcb79c80
3
+ metadata.gz: 4af26e2ed8bedd59ea727cc40967738a53967495d7109401f3698895e4cc394e
4
+ data.tar.gz: 356249bc613ab922c13deb3d9b8f91fd82fd99984b3e44e258b4d04720ab49c8
5
5
  SHA512:
6
- metadata.gz: 91294088c5a975d6604dec889882f675cdc2b2d2514448035e257600c7d9e31ff1ca7402d6c85a39d2b623ea06d6148f6766a15a338e95d3cd6a19a880d2e001
7
- data.tar.gz: d248d7914f3134f59202efa1a9d6519ae770183bb1f2296456e82956a0c2b7552291cecbfc960ca32893f90ce2f2c3eb4649fef6892664d1d3a3603b19993596
6
+ metadata.gz: 27b9d07aa2667fefb5d6f5f2acba706a1397da9c37af28bcf8f775753bf183507acbd4a5cf6604c16e5b7017b34a7b1c430d13db0407144038d46807a6ced3aa
7
+ data.tar.gz: 456c8e4ccffd562f766a732940b1c0378806bd71b18b223de6d3926ef0fafc956856d1bfbdde93ff9287857a5908bc1ac8715218dc88d5fed5b15b3ac6c9c5dc
data/.gitignore CHANGED
@@ -9,3 +9,7 @@
9
9
  /tmp/
10
10
 
11
11
  /spec/dummy/config/database.yml
12
+
13
+ # Ignore vim swap files
14
+ *.swp
15
+ *.swo
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in recorder.gemspec
data/README.md CHANGED
@@ -20,7 +20,37 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- TODO: Write usage instructions here
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](http://www.jetrockets.ru/jetrockets.png)
67
+ ![JetRockets](https://media.jetrockets.pro/jetrockets-white.png)
38
68
 
39
- Recorder is maintained by [JetRockets](http://www.jetrockets.ru).
69
+ Recorder is maintained by [JetRockets](https://www.jetrockets.pro]).
40
70
 
41
71
  ## License
42
72
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "recorder"
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 "irb"
14
+ require 'irb'
14
15
  IRB.start
@@ -1,48 +1,50 @@
1
- require "rails/generators"
2
- require "rails/generators/active_record"
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("../templates", __FILE__)
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: "Create partitions to `recorder_revisions` table"
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: "Add `number` column to `recorder_revisions` table"
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: "Add index by `user_id` column to `recorder_revisions` table"
31
+ desc: 'Add index by `user_id` column to `recorder_revisions` table'
30
32
  )
31
33
 
32
- desc "Generates (but does not run) a migration to add a `recorder_revisions` table."
34
+ desc 'Generates (but does not run) a migration to add a `recorder_revisions` table.'
33
35
 
34
36
  def create_migration_file
35
- self.add_or_skip_recorder_migration('create_recorder_revisions')
36
- self.add_or_skip_recorder_migration('add_number_column_to_recorder_revisions') if options.with_number_column?
37
- self.add_or_skip_recorder_migration('add_index_by_user_id_to_recorder_revisions') if options.with_index_by_user_id?
38
- self.add_or_skip_recorder_migration('add_partitions_to_recorder_revisions') if options.with_partitions?
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
- protected
47
+ protected
46
48
 
47
49
  def add_or_skip_recorder_migration(template)
48
50
  migration_dir = File.expand_path('db/migrate')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This migration adds number column to the `revisions` table.
2
4
  class AddIndexByUserIdToRecorderRevisions < ActiveRecord::Migration
3
5
  def change
@@ -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, :null => false, :default => 0
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
- RETURN NEW;
20
- END;
21
- $BODY$ LANGUAGE plpgsql;
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 <<-SQL
25
- CREATE TRIGGER update_recorder_revisions_number
26
- BEFORE INSERT ON recorder_revisions FOR EACH ROW
27
- EXECUTE PROCEDURE get_recorder_revisions_number();
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, null: false
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, [:item_type, :item_id]
18
+ add_index :recorder_revisions, %i[item_type item_id]
17
19
  end
18
20
  end
@@ -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 = Hash[changes]
8
+ @item = item
9
+ @changes = changes.to_h
8
10
  end
9
11
 
10
12
  def keys
11
- self.changes.try(:keys) || []
13
+ changes.try(:keys) || []
12
14
  end
13
15
 
14
16
  def human_attribute_name(attribute)
15
- if defined?(Draper) && self.item.decorated?
16
- self.item.source.class.human_attribute_name(attribute.to_s)
17
+ if defined?(Draper) && item.decorated?
18
+ item.source.class.human_attribute_name(attribute.to_s)
17
19
  else
18
- self.item.class.human_attribute_name(attribute.to_s)
20
+ item.class.human_attribute_name(attribute.to_s)
19
21
  end
20
22
  end
21
23
 
22
24
  def previous(attribute)
23
- self.try("previous_#{attribute}") || self.previous_version.try(attribute)
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 = self.item.dup
31
+ @previous_version = item.dup
30
32
 
31
- self.changes.each do |key, change|
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
- self.try("next_#{attribute}") || self.next_version.try(attribute)
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 = self.item.dup
47
+ @next_version = item.dup
46
48
 
47
- self.changes.each do |key, change|
49
+ changes.each do |key, change|
48
50
  @next_version.send("#{key}=", change[1])
49
51
  end
50
52
 
@@ -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, :ignore
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
- :queue => 'recorder',
16
- :retry => 10,
17
- :backtrace => true
18
+ queue: 'recorder',
19
+ retry: 10,
20
+ backtrace: true
18
21
  }
19
22
 
20
- @ignore = Array.new
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
@@ -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, :class_name => '::Recorder::Revision', :inverse_of => :item, :as => :item do
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 self.recorder_dirty?
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 self.recorder_dirty?
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 self.recorder_dirty?
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