camel_trail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 74fe79d392691d6f939590e1742163bc77d2379982d238e36b626cd03f657636
4
+ data.tar.gz: bc8a4444544e8f23c7312c5951e91026689d3b79aef0a3b51b7d872539422767
5
+ SHA512:
6
+ metadata.gz: 93cf6bcfd5c406f8c3b9489d89cd831c59fc414aeffd35f880a4b4db63a6d3067a257ccdd68744241d07a2a801c2193f7858eec12c02830054ae50628916212e
7
+ data.tar.gz: 6995ef5aad5760118c4b1bc450655b009c559958737707a81d45db513dd6487d646bfe6b7be4fa2cf04351d6e7c85a684098b5626320d18c10cc5cb449f4b148
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env rake
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
6
+ require "rubocop/rake_task"
7
+
8
+ begin
9
+ require "bundler/setup"
10
+ require "yard"
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.files = ["lib/**/*.rb"]
13
+ t.files = ["app/models/camel_trail/*.rb"]
14
+ end
15
+ rescue LoadError
16
+ puts "You must `bundle install` to run rake tasks"
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
20
+ load "rails/tasks/engine.rake"
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ RSpec::Core::RakeTask.new(:spec)
25
+
26
+ RuboCop::RakeTask.new(:rubocop)
27
+
28
+ task default: %i[rubocop app:db:prepare spec]
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ class EncryptedHistory < ::CamelTrail::History
5
+ extend AttrEncrypted
6
+
7
+ attr_encrypted :note, key: NitroConfig.get_deferred!("encryption_key"),
8
+ encode: true,
9
+ insecure_mode: true,
10
+ algorithm: "aes-256-cbc",
11
+ mode: :single_iv_and_salt
12
+
13
+ before_save :clear_plain_note
14
+
15
+ private
16
+
17
+ def clear_plain_note
18
+ self[:note] = nil
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ class History < ::CamelTrail::ApplicationRecord
5
+ serialize :source_changes, ::CamelTrail::YAMLUnsafeCoder
6
+ serialize :backtrace, Array
7
+
8
+ default_scope { order("id DESC") }
9
+
10
+ scope :for_source, ->(source_object) do
11
+ where(source_id: source_object.id, source_type: source_object.class.to_s)
12
+ end
13
+
14
+ scope :in_natural_order, -> { reorder("id ASC") }
15
+
16
+ before_create :set_backtrace
17
+
18
+ private
19
+
20
+ def set_backtrace
21
+ self.backtrace = CamelTrail::Config.backtrace_cleaner&.clean(caller) || caller
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ development:
2
+ adapter: sqlite3
3
+ database: db/camel_trail_development.sqlite
4
+
5
+ test:
6
+ adapter: sqlite3
7
+ database: db/camel_trail_test.sqlite
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/MethodLength
4
+ class CreateCamelTrails < ActiveRecord::Migration[6.0]
5
+ def change
6
+ create_table :"#{CamelTrail.table_name_prefix}histories" do |t|
7
+ t.string :source_type
8
+ t.string :source_id
9
+ t.text :source_changes
10
+ t.integer :user_id
11
+ t.text :note
12
+ t.string :activity
13
+ t.text :encrypted_note
14
+ t.text :backtrace
15
+ end
16
+
17
+ add_index :"#{CamelTrail.table_name_prefix}histories", %i[source_type source_id]
18
+ end
19
+ end
20
+ # rubocop:enable Metrics/MethodLength
data/docs/README.md ADDED
@@ -0,0 +1,74 @@
1
+ ## What is CamelTrail
2
+
3
+ CamelTrail makes it easy to keep a history of attribute changes on a model
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'camel_trail'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install camel_trail
20
+
21
+
22
+ # Configuration
23
+
24
+ 1. Add CamelTrail config to your initializer file.
25
+ 2. You need to set `current_session_user_id` to include user info in `camel_trail_histories` table.
26
+ 3. You can optionally set `table_name_prefix` to customize default table name. Defaults to `camel_trail_histories`.
27
+ 4. CamelTrail stores backtrace info in `camel_trail_histories`. It defaults to `Rails.backtrace_cleaner`. You can optionally set it to your customized backtrace cleaner.
28
+
29
+ ```ruby
30
+ CamelTrail.config do
31
+ table_name_prefix "myapp_"
32
+ current_session_user_id { MyApp.current_session_user_id }
33
+ backtrace_cleaner { YourCustom.backtrace_cleaner }
34
+ end
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```ruby
40
+ class Project < ApplicationRecord
41
+   include ::CamelTrail::Recordable
42
+ end
43
+ ```
44
+
45
+ Inlcude `camel_trail` in your lib files if you need to call it there:
46
+
47
+ ```ruby
48
+ require "camel_trail"
49
+ ```
50
+
51
+
52
+ Now you can access the object history through `CamelTrail.for(object)` like:
53
+
54
+ ```ruby
55
+ project = Project.create
56
+ CamelTrail.for(project).size
57
+ # => 1
58
+ ```
59
+
60
+ The user performing the action will be recorded from the Thread local `:user_id`.
61
+
62
+ Then, require the engine in your `application.rb`
63
+
64
+ ```ruby
65
+ require "camel_trail"
66
+ ```
67
+
68
+ ## Contributing
69
+
70
+ Bug reports and pull requests are welcome on GitHub at https://github.com/powerhome/power-tools.
71
+
72
+ ## License
73
+
74
+ The package is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ module Config
5
+ module_function
6
+
7
+ # CamelTrail Stores backtrace info in `camel_trail_histories`
8
+ # It's config defaults to `Rails.backtrace_cleaner`
9
+ # You can optionally set it to your customized backtrace cleaner
10
+ # via `CamelTrail.config.backtrace_cleaner = MyBacktraceCleaner`
11
+ mattr_accessor :backtrace_cleaner
12
+
13
+ # Allows to set configurion for CamelTrail
14
+ def config(&block)
15
+ class_eval(&block)
16
+ end
17
+
18
+ # Optionally set `table_name_prefix` to customize default table name.
19
+ # Defaults to `camel_trail_histories`
20
+ def table_name_prefix(value)
21
+ CamelTrail.table_name_prefix = value
22
+ end
23
+
24
+ # Sets `current_session_user_id` to include user info in `camel_trail_histories` table.
25
+ def current_session_user_id(&block)
26
+ @current_session_user_id = block if block
27
+ @current_session_user_id
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace CamelTrail
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ config.generators do |g|
12
+ g.fixture_replacement :factory_bot, dir: "spec/factories"
13
+ end
14
+
15
+ initializer :append_migrations do |app|
16
+ unless app.root.to_s.match? root.to_s
17
+ config.paths["db/migrate"].expanded.each do |expanded_path|
18
+ app.config.paths["db/migrate"] << expanded_path
19
+ end
20
+ end
21
+ end
22
+
23
+ initializer "camel_trail.backtrace_cleaner" do
24
+ CamelTrail::Config.backtrace_cleaner ||= Rails.backtrace_cleaner
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ class EntryPresenter
5
+ delegate :backtrace,
6
+ :created_at,
7
+ :activity,
8
+ :source_type,
9
+ :source_id,
10
+ :source_changes,
11
+ :user_id,
12
+ :user,
13
+ :note,
14
+ :id,
15
+ to: :history, prefix: false
16
+ def initialize(history)
17
+ @history = history
18
+ end
19
+
20
+ # Details the changes made to the source object.
21
+ def describe_changes
22
+ @history.source_changes.map do |field, change|
23
+ describe_change(field, change)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :history
30
+
31
+ def describe_change(field, change)
32
+ values = change.map { |value| format_value(field, value) }
33
+ return unless values.any?
34
+
35
+ from, to = *values
36
+ to = "nothing" if to.blank?
37
+ build_message(field, from, to)
38
+ rescue
39
+ "#{field}: #{from} - #{to}"
40
+ end
41
+
42
+ def format_value(field, val)
43
+ val = val.strftime("%m/%d/%Y") if val.respond_to?(:strftime)
44
+ val = val.map(&:humanize).join(", ") if val.is_a?(Array) && val.first.is_a?(String)
45
+ val = number_to_currency(val, precision: 2) if numeric?(field, val)
46
+ val
47
+ end
48
+
49
+ def build_message(field, from, to)
50
+ if to == true
51
+ "Selected #{field.to_s.titleize}"
52
+ elsif from == true
53
+ "Deselected #{field.to_s.titleize}"
54
+ elsif from.present?
55
+ "Changed #{field.to_s.titleize} from #{from} to #{to}"
56
+ else
57
+ "Changed #{field.to_s.titleize} to #{to}"
58
+ end
59
+ end
60
+
61
+ def numeric?(field, val)
62
+ field =~ /amount|price|cost|ceiling/i && val.respond_to?(:to_f) && val != ""
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ module Recordable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ around_save :__record_changes
9
+ end
10
+
11
+ class_methods do
12
+ attr_reader :__camel_trail_source_changes
13
+
14
+ def history_options(source_changes: nil)
15
+ @__camel_trail_source_changes = source_changes
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def __record_changes
22
+ activity = new_record? ? :created : :updated
23
+ yield
24
+ CamelTrail.record!(self, activity, __camel_trail_source_changes,
25
+ CamelTrail::Config.current_session_user_id&.call)
26
+ end
27
+
28
+ def __camel_trail_source_changes
29
+ source_changes = self.class.__camel_trail_source_changes
30
+
31
+ case source_changes
32
+ when Proc then instance_exec(&source_changes)
33
+ when Symbol then send(source_changes)
34
+ else saved_changes
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ # The current version of the gem.
5
+ VERSION = "0.0.1"
6
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamelTrail
4
+ module YAMLUnsafeCoder
5
+ module_function
6
+
7
+ # Loads the object from YAML.
8
+ def load(payload)
9
+ return unless payload
10
+
11
+ if YAML.respond_to?(:unsafe_load)
12
+ YAML.unsafe_load(payload)
13
+ else
14
+ YAML.load(payload) # rubocop:disable Security/YAMLLoad
15
+ end
16
+ end
17
+
18
+ # Dumps the object to YAML.
19
+ def dump(obj)
20
+ YAML.dump obj
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "attr_encrypted"
4
+ require "nitro_config"
5
+ require "camel_trail/engine"
6
+ require "camel_trail/entry_presenter"
7
+ require "camel_trail/recordable"
8
+ require "camel_trail/yaml_unsafe_coder"
9
+ require "camel_trail/config"
10
+
11
+ module CamelTrail
12
+ module_function
13
+
14
+ mattr_accessor(:table_name_prefix) { "camel_trail_" }
15
+
16
+ # Allows to set configurion for CamelTrail
17
+ #
18
+ # CamelTrail.config do
19
+ # configs to be set
20
+ # end
21
+ def config(...)
22
+ Config.config(...)
23
+ end
24
+
25
+ # Records a history activity for the given object
26
+ #
27
+ # @param object [#id] the object recording a history
28
+ # @param activity [Symbol] the activity that generated the history entry (i.e.: :created)
29
+ # @param changes [Hash] an ActiveRecord changes hash (i.e.: { 'name' => ['old', 'new'] })
30
+ # @param user_id [Integer] the id of the user triggered the history entry
31
+ # @param note [String] a note that can be attached to a history
32
+ # @param encrypted [Boolean] whether to encrypt or not the note
33
+ # @return [CamelTrail::EntryPresenter]
34
+
35
+ # rubocop:disable Metrics/ParameterLists
36
+ def record!(object, activity, changes, user_id, note = nil, encrypted: false)
37
+ klass = encrypted ? CamelTrail::EncryptedHistory : CamelTrail::History
38
+
39
+ history = klass.for_source(object).create!(
40
+ source_changes: changes,
41
+ activity: activity,
42
+ user_id: user_id,
43
+ note: note
44
+ )
45
+ EntryPresenter.new(history)
46
+ end
47
+ # rubocop:enable Metrics/ParameterLists
48
+
49
+ # A collection of the history entries associated with the object
50
+ #
51
+ # @param object [#id] the object recording a history
52
+ # @return [Array<CamelTrail::EntryPresenter>]
53
+ def for(object, encrypted: false, in_natural_order: false)
54
+ klass = encrypted ? CamelTrail::EncryptedHistory : CamelTrail::History
55
+
56
+ history_collection = in_natural_order ? klass.for_source(object).in_natural_order : klass.for_source(object)
57
+ history_collection.to_a.map do |history|
58
+ CamelTrail::EntryPresenter.new(history)
59
+ end
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: camel_trail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Gurban Haydarov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nitro_config
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: powerhome-attr_encrypted
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 6.0.6.1
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '7.0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 6.0.6.1
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '7.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: license_finder
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '7.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '7.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '13'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec-rails
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 5.1.2
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 5.1.2
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop-powerhome
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '='
122
+ - !ruby/object:Gem::Version
123
+ version: 0.5.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - '='
129
+ - !ruby/object:Gem::Version
130
+ version: 0.5.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: simplecov
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '='
136
+ - !ruby/object:Gem::Version
137
+ version: 0.15.1
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '='
143
+ - !ruby/object:Gem::Version
144
+ version: 0.15.1
145
+ - !ruby/object:Gem::Dependency
146
+ name: sqlite3
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 1.4.2
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 1.4.2
159
+ - !ruby/object:Gem::Dependency
160
+ name: yard
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - '='
164
+ - !ruby/object:Gem::Version
165
+ version: 0.9.21
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - '='
171
+ - !ruby/object:Gem::Version
172
+ version: 0.9.21
173
+ description: CamelTrail makes it easy to keep a history of attribute changes on a
174
+ model
175
+ email:
176
+ - gurban@hey.com
177
+ executables: []
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - Rakefile
182
+ - app/models/camel_trail/application_record.rb
183
+ - app/models/camel_trail/encrypted_history.rb
184
+ - app/models/camel_trail/history.rb
185
+ - config/database.yml
186
+ - db/migrate/20200514201449_create_camel_trails.rb
187
+ - docs/README.md
188
+ - lib/camel_trail.rb
189
+ - lib/camel_trail/config.rb
190
+ - lib/camel_trail/engine.rb
191
+ - lib/camel_trail/entry_presenter.rb
192
+ - lib/camel_trail/recordable.rb
193
+ - lib/camel_trail/version.rb
194
+ - lib/camel_trail/yaml_unsafe_coder.rb
195
+ homepage: https://github.com/powerhome/power-tools
196
+ licenses:
197
+ - MIT
198
+ metadata:
199
+ rubygems_mfa_required: 'true'
200
+ post_install_message:
201
+ rdoc_options: []
202
+ require_paths:
203
+ - lib
204
+ required_ruby_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '2.7'
209
+ required_rubygems_version: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
214
+ requirements: []
215
+ rubygems_version: 3.4.6
216
+ signing_key:
217
+ specification_version: 4
218
+ summary: Simple way to track database changes
219
+ test_files: []