camel_trail 0.0.1

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 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: []