trakable 0.2.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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +81 -0
  3. data/CHANGELOG.md +50 -0
  4. data/LICENSE +21 -0
  5. data/README.md +330 -0
  6. data/Rakefile +16 -0
  7. data/benchmark/full_benchmark.rb +221 -0
  8. data/benchmark/integration_memory.rb +70 -0
  9. data/benchmark/memory_benchmark.rb +141 -0
  10. data/benchmark/perf_benchmark.rb +130 -0
  11. data/integration/README.md +65 -0
  12. data/integration/run_all.rb +62 -0
  13. data/integration/scenarios/01-basic-tracking/scenario.rb +51 -0
  14. data/integration/scenarios/02-revert-restoration/scenario.rb +103 -0
  15. data/integration/scenarios/03-whodunnit-tracking/scenario.rb +72 -0
  16. data/integration/scenarios/04-cleanup-retention/scenario.rb +66 -0
  17. data/integration/scenarios/05-without-tracking/scenario.rb +62 -0
  18. data/integration/scenarios/06-callback-lifecycle/scenario.rb +103 -0
  19. data/integration/scenarios/07-global-config/scenario.rb +52 -0
  20. data/integration/scenarios/08-controller-integration/scenario.rb +44 -0
  21. data/integration/scenarios/09-cleanup-max-traks/scenario.rb +58 -0
  22. data/integration/scenarios/10-model-configuration/scenario.rb +68 -0
  23. data/integration/scenarios/11-conditional-tracking/scenario.rb +48 -0
  24. data/integration/scenarios/12-metadata/scenario.rb +54 -0
  25. data/integration/scenarios/13-traks-association/scenario.rb +80 -0
  26. data/integration/scenarios/14-time-travel/scenario.rb +132 -0
  27. data/integration/scenarios/15-diffing-changeset/scenario.rb +109 -0
  28. data/integration/scenarios/16-serialization/scenario.rb +159 -0
  29. data/integration/scenarios/17-associations-tracking/scenario.rb +143 -0
  30. data/integration/scenarios/18-bulk-operations/scenario.rb +70 -0
  31. data/integration/scenarios/19-transactions/scenario.rb +89 -0
  32. data/integration/scenarios/20-performance/scenario.rb +89 -0
  33. data/integration/scenarios/21-storage-backends/scenario.rb +52 -0
  34. data/integration/scenarios/22-multi-tenancy/scenario.rb +49 -0
  35. data/integration/scenarios/23-sti/scenario.rb +58 -0
  36. data/integration/scenarios/24-edge-cases-part1/scenario.rb +86 -0
  37. data/integration/scenarios/25-edge-cases-part2/scenario.rb +74 -0
  38. data/integration/scenarios/26-edge-cases-part3/scenario.rb +76 -0
  39. data/integration/scenarios/27-api-query-interface/scenario.rb +78 -0
  40. data/integration/scenarios/28-security-compliance/scenario.rb +61 -0
  41. data/integration/scenarios/29-soft-delete/scenario.rb +43 -0
  42. data/integration/scenarios/30-custom-events/scenario.rb +45 -0
  43. data/integration/scenarios/31-gem-packaging/scenario.rb +58 -0
  44. data/integration/scenarios/32-bypass-fail-closed/scenario.rb +77 -0
  45. data/integration/scenarios/33-coexistence-standalone/scenario.rb +53 -0
  46. data/integration/scenarios/34-real-tracking/scenario.rb +254 -0
  47. data/integration/scenarios/35-revert-undo/scenario.rb +235 -0
  48. data/integration/scenarios/36-whodunnit-deep/scenario.rb +281 -0
  49. data/integration/scenarios/37-real-world-use-cases/scenario.rb +1213 -0
  50. data/integration/scenarios/38-concurrency/scenario.rb +163 -0
  51. data/integration/scenarios/39-query-scopes/scenario.rb +126 -0
  52. data/integration/scenarios/40-whodunnit-config/scenario.rb +113 -0
  53. data/integration/scenarios/41-batch-cleanup/scenario.rb +186 -0
  54. data/integration/scenarios/scenario_runner.rb +68 -0
  55. data/lib/generators/trakable/install_generator.rb +28 -0
  56. data/lib/generators/trakable/templates/create_traks_migration.rb +23 -0
  57. data/lib/generators/trakable/templates/trakable_initializer.rb +15 -0
  58. data/lib/trakable/cleanup.rb +89 -0
  59. data/lib/trakable/config.rb +22 -0
  60. data/lib/trakable/context.rb +85 -0
  61. data/lib/trakable/controller.rb +25 -0
  62. data/lib/trakable/model.rb +99 -0
  63. data/lib/trakable/railtie.rb +28 -0
  64. data/lib/trakable/revertable.rb +166 -0
  65. data/lib/trakable/tracker.rb +134 -0
  66. data/lib/trakable/trak.rb +98 -0
  67. data/lib/trakable/version.rb +5 -0
  68. data/lib/trakable.rb +51 -0
  69. data/trakable.gemspec +41 -0
  70. metadata +242 -0
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trakable
4
+ # Trak model for storing audit records.
5
+ # Each trak represents a single state change event on a tracked item.
6
+ #
7
+ # Inherits from ActiveRecord::Base when available (Rails apps),
8
+ # otherwise works as a plain Ruby object (testing, non-AR contexts).
9
+ #
10
+ class Trak < (defined?(ActiveRecord::Base) ? ActiveRecord::Base : Object)
11
+ include Revertable
12
+
13
+ EVENTS = %w[create update destroy].freeze
14
+
15
+ if defined?(ActiveRecord::Base) && self < ActiveRecord::Base
16
+ require 'json'
17
+
18
+ self.table_name = 'traks'
19
+
20
+ serialize :object, coder: JSON
21
+ serialize :changeset, coder: JSON
22
+ serialize :metadata, coder: JSON
23
+
24
+ belongs_to :item, polymorphic: true, optional: true
25
+ belongs_to :whodunnit, polymorphic: true, optional: true
26
+
27
+ scope :for_item_type, ->(type) { where(item_type: type.to_s) }
28
+ scope :for_event, ->(event) { where(event: event.to_s) }
29
+ scope :for_whodunnit, ->(user) { where(whodunnit_type: user.class.name, whodunnit_id: user.id) }
30
+ scope :created_before, ->(time) { where(arel_table[:created_at].lt(time)) }
31
+ scope :created_after, ->(time) { where(arel_table[:created_at].gt(time)) }
32
+ scope :recent, -> { order(created_at: :desc) }
33
+ else
34
+ ATTRS = %i[id item_type item_id event object changeset
35
+ whodunnit_type whodunnit_id metadata created_at].freeze
36
+
37
+ # Pre-computed ivar symbols to avoid string interpolation in initialize
38
+ ATTR_IVARS = ATTRS.to_h { |a| [a, :"@#{a}"] }.freeze
39
+
40
+ attr_accessor(*ATTRS)
41
+
42
+ def initialize(attrs = {})
43
+ attrs.each do |key, value|
44
+ ivar = ATTR_IVARS[key]
45
+ instance_variable_set(ivar, value) if ivar
46
+ end
47
+ end
48
+
49
+ def self.table_name
50
+ 'traks'
51
+ end
52
+
53
+ def item
54
+ return nil unless item_type && item_id
55
+
56
+ @item ||= item_type.constantize.find_by(id: item_id)
57
+ rescue NameError
58
+ nil
59
+ end
60
+
61
+ def whodunnit
62
+ return nil unless whodunnit_type && whodunnit_id
63
+
64
+ @whodunnit ||= whodunnit_type.constantize.find_by(id: whodunnit_id)
65
+ rescue NameError
66
+ nil
67
+ end
68
+ end
69
+
70
+ class << self
71
+ def build(item:, event:, changeset:, object: nil, whodunnit: nil, metadata: nil)
72
+ new(
73
+ item_type: item.class.name,
74
+ item_id: item.id,
75
+ event: event,
76
+ object: object,
77
+ changeset: changeset,
78
+ whodunnit_type: whodunnit&.class&.name,
79
+ whodunnit_id: whodunnit&.id,
80
+ metadata: metadata,
81
+ created_at: Time.now
82
+ )
83
+ end
84
+ end
85
+
86
+ def create?
87
+ event == 'create'
88
+ end
89
+
90
+ def update?
91
+ event == 'update'
92
+ end
93
+
94
+ def destroy?
95
+ event == 'destroy'
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trakable
4
+ VERSION = '0.2.0'
5
+ end
data/lib/trakable.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'trakable/version'
4
+
5
+ # Trakable provides audit logging and version tracking for ActiveRecord models.
6
+ # It offers polymorphic whodunnit tracking, changesets, and built-in retention.
7
+ module Trakable
8
+ @configuration = nil
9
+
10
+ class << self
11
+ # Returns the global configuration.
12
+ # Eagerly initialized after require to eliminate thread-safety race.
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield(configuration)
19
+ end
20
+
21
+ def enabled?
22
+ configuration.enabled
23
+ end
24
+
25
+ def with_user(user, &)
26
+ Context.with_user(user, &)
27
+ end
28
+
29
+ def with_tracking(&)
30
+ Context.with_tracking(&)
31
+ end
32
+
33
+ def without_tracking(&)
34
+ Context.without_tracking(&)
35
+ end
36
+ end
37
+ end
38
+
39
+ require_relative 'trakable/cleanup'
40
+ require_relative 'trakable/config'
41
+ require_relative 'trakable/context'
42
+ Trakable.autoload :Controller, 'trakable/controller'
43
+ require_relative 'trakable/model'
44
+ require_relative 'trakable/revertable'
45
+ require_relative 'trakable/trak'
46
+ require_relative 'trakable/tracker'
47
+
48
+ require_relative 'trakable/railtie' if defined?(Rails::Railtie)
49
+
50
+ # Eager-initialize configuration to eliminate thread-safety race on first access
51
+ Trakable.configuration
data/trakable.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/trakable/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'trakable'
7
+ spec.version = Trakable::VERSION
8
+ spec.authors = ['Hadrien Blanc']
9
+ spec.email = ['trakable@hadrienblanc.com']
10
+
11
+ spec.summary = 'Audit and versioning for ActiveRecord models'
12
+ spec.description = 'Trakable provides audit logging and version tracking for ActiveRecord models with polymorphic whodunnit, changesets, and built-in retention'
13
+ spec.homepage = 'https://github.com/hadrienblanc/trakable'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.1.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be included in the gem
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'activerecord', '>= 7.1', '< 8.2'
33
+ spec.add_dependency 'activesupport', '>= 7.1', '< 8.2'
34
+
35
+ spec.add_development_dependency 'minitest', '~> 5.25'
36
+ spec.add_development_dependency 'railties', '>= 7.1', '< 8.2'
37
+ spec.add_development_dependency 'rake', '~> 13.0'
38
+ spec.add_development_dependency 'rubocop', '~> 1.69'
39
+ spec.add_development_dependency 'rubocop-minitest', '~> 0.37'
40
+ spec.add_development_dependency 'sqlite3', '~> 2.5'
41
+ end
metadata ADDED
@@ -0,0 +1,242 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: trakable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Hadrien Blanc
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '8.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '8.2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activesupport
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '7.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '8.2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '7.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '8.2'
52
+ - !ruby/object:Gem::Dependency
53
+ name: minitest
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '5.25'
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '5.25'
66
+ - !ruby/object:Gem::Dependency
67
+ name: railties
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '7.1'
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '8.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '7.1'
83
+ - - "<"
84
+ - !ruby/object:Gem::Version
85
+ version: '8.2'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rake
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '13.0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '13.0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: rubocop
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '1.69'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '1.69'
114
+ - !ruby/object:Gem::Dependency
115
+ name: rubocop-minitest
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '0.37'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '0.37'
128
+ - !ruby/object:Gem::Dependency
129
+ name: sqlite3
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '2.5'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '2.5'
142
+ description: Trakable provides audit logging and version tracking for ActiveRecord
143
+ models with polymorphic whodunnit, changesets, and built-in retention
144
+ email:
145
+ - trakable@hadrienblanc.com
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - ".rubocop.yml"
151
+ - CHANGELOG.md
152
+ - LICENSE
153
+ - README.md
154
+ - Rakefile
155
+ - benchmark/full_benchmark.rb
156
+ - benchmark/integration_memory.rb
157
+ - benchmark/memory_benchmark.rb
158
+ - benchmark/perf_benchmark.rb
159
+ - integration/README.md
160
+ - integration/run_all.rb
161
+ - integration/scenarios/01-basic-tracking/scenario.rb
162
+ - integration/scenarios/02-revert-restoration/scenario.rb
163
+ - integration/scenarios/03-whodunnit-tracking/scenario.rb
164
+ - integration/scenarios/04-cleanup-retention/scenario.rb
165
+ - integration/scenarios/05-without-tracking/scenario.rb
166
+ - integration/scenarios/06-callback-lifecycle/scenario.rb
167
+ - integration/scenarios/07-global-config/scenario.rb
168
+ - integration/scenarios/08-controller-integration/scenario.rb
169
+ - integration/scenarios/09-cleanup-max-traks/scenario.rb
170
+ - integration/scenarios/10-model-configuration/scenario.rb
171
+ - integration/scenarios/11-conditional-tracking/scenario.rb
172
+ - integration/scenarios/12-metadata/scenario.rb
173
+ - integration/scenarios/13-traks-association/scenario.rb
174
+ - integration/scenarios/14-time-travel/scenario.rb
175
+ - integration/scenarios/15-diffing-changeset/scenario.rb
176
+ - integration/scenarios/16-serialization/scenario.rb
177
+ - integration/scenarios/17-associations-tracking/scenario.rb
178
+ - integration/scenarios/18-bulk-operations/scenario.rb
179
+ - integration/scenarios/19-transactions/scenario.rb
180
+ - integration/scenarios/20-performance/scenario.rb
181
+ - integration/scenarios/21-storage-backends/scenario.rb
182
+ - integration/scenarios/22-multi-tenancy/scenario.rb
183
+ - integration/scenarios/23-sti/scenario.rb
184
+ - integration/scenarios/24-edge-cases-part1/scenario.rb
185
+ - integration/scenarios/25-edge-cases-part2/scenario.rb
186
+ - integration/scenarios/26-edge-cases-part3/scenario.rb
187
+ - integration/scenarios/27-api-query-interface/scenario.rb
188
+ - integration/scenarios/28-security-compliance/scenario.rb
189
+ - integration/scenarios/29-soft-delete/scenario.rb
190
+ - integration/scenarios/30-custom-events/scenario.rb
191
+ - integration/scenarios/31-gem-packaging/scenario.rb
192
+ - integration/scenarios/32-bypass-fail-closed/scenario.rb
193
+ - integration/scenarios/33-coexistence-standalone/scenario.rb
194
+ - integration/scenarios/34-real-tracking/scenario.rb
195
+ - integration/scenarios/35-revert-undo/scenario.rb
196
+ - integration/scenarios/36-whodunnit-deep/scenario.rb
197
+ - integration/scenarios/37-real-world-use-cases/scenario.rb
198
+ - integration/scenarios/38-concurrency/scenario.rb
199
+ - integration/scenarios/39-query-scopes/scenario.rb
200
+ - integration/scenarios/40-whodunnit-config/scenario.rb
201
+ - integration/scenarios/41-batch-cleanup/scenario.rb
202
+ - integration/scenarios/scenario_runner.rb
203
+ - lib/generators/trakable/install_generator.rb
204
+ - lib/generators/trakable/templates/create_traks_migration.rb
205
+ - lib/generators/trakable/templates/trakable_initializer.rb
206
+ - lib/trakable.rb
207
+ - lib/trakable/cleanup.rb
208
+ - lib/trakable/config.rb
209
+ - lib/trakable/context.rb
210
+ - lib/trakable/controller.rb
211
+ - lib/trakable/model.rb
212
+ - lib/trakable/railtie.rb
213
+ - lib/trakable/revertable.rb
214
+ - lib/trakable/tracker.rb
215
+ - lib/trakable/trak.rb
216
+ - lib/trakable/version.rb
217
+ - trakable.gemspec
218
+ homepage: https://github.com/hadrienblanc/trakable
219
+ licenses:
220
+ - MIT
221
+ metadata:
222
+ homepage_uri: https://github.com/hadrienblanc/trakable
223
+ source_code_uri: https://github.com/hadrienblanc/trakable
224
+ changelog_uri: https://github.com/hadrienblanc/trakable/blob/main/CHANGELOG.md
225
+ rdoc_options: []
226
+ require_paths:
227
+ - lib
228
+ required_ruby_version: !ruby/object:Gem::Requirement
229
+ requirements:
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: 3.1.0
233
+ required_rubygems_version: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: '0'
238
+ requirements: []
239
+ rubygems_version: 3.6.9
240
+ specification_version: 4
241
+ summary: Audit and versioning for ActiveRecord models
242
+ test_files: []