waldit 0.0.1 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afad3b16943e3984c6584b02ef8edb2fb8abb8413687726a657110018e9fcbd4
4
- data.tar.gz: 934aacd6442c00b7fd40287360e2d8a1b52e0f37328711e029d36812d9cf94d1
3
+ metadata.gz: fb7ae260f6d1af42f4e6169084c7ba79de48cea93f44ff47be032f5764df23a6
4
+ data.tar.gz: 7cf9d0c022ffbb457bcac4380ed635cd65d2df1db24faae5fbd169b50b5aa07c
5
5
  SHA512:
6
- metadata.gz: ff61b8c0d473390f5197ae4544ad932014483ba68cdb8972e79eee35d4747814c5e64ae60cc53a078653de82f483ae94e98db69b40796a8be0d38b1a9db694d1
7
- data.tar.gz: a9e6c961b67f1be3335d773af2c42d76b7f74fd5d0eb48bd3cb5dce4cf355ce88144caa626ede05de357af47ce03986893bd0b9accab687bf4490c077fc8c787
6
+ metadata.gz: b54045c2265401d420a276efddb20525b7b72e39d27c25e10da6a7a2c357805ea1956aed900d3c265f5cc88096dc402cd53ea52d0989eff8e3dd965d47c2ec75
7
+ data.tar.gz: 8d86ba10a3c94e7ca87bb90a39957663b305f19b6b5a8bfb7062fdae89879b57c82d230989bfd49b5480738f63ca81bf9db7b1093406b730578498bfc001a862
data/README.md CHANGED
@@ -1 +1,117 @@
1
1
  # Waldit
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/waldit.svg)](https://badge.fury.io/rb/waldit)
4
+
5
+ Waldit is a Ruby gem that provides a simple and extensible way to audit changes to your ActiveRecord models. It leverages PostgreSQL's logical replication capabilities to capture changes directly from your database with 100% consistency.
6
+
7
+ ## Features
8
+
9
+ - **Automatic Auditing:** Automatically track `create`, `update`, and `delete` operations on your models.
10
+ - **Contextual Auditing:** Add custom context to your audit records to understand who made the change and why.
11
+ - **Flexible Configuration:** Configure which tables and columns to watch, and how to store audit information.
12
+ - **High Performance:** Built on top of [`wal`](https://github.com/reu/wal), which uses PostgreSQL's logical replication for minimal overhead.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "waldit"
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install waldit
29
+
30
+ ## Usage
31
+
32
+ 1. **Configure your database adapter:**
33
+
34
+ First step is to configure in your `config/database.yml` and change your adapter to `postgresqlwaldit`, which is a special adapter that allows injecting `waldit` contextual information on your transactions:
35
+
36
+ ```yaml
37
+ default: &default
38
+ adapter: postgresqlwaldit
39
+ # ...
40
+ ```
41
+
42
+ 2. **Create an audit table:**
43
+
44
+ Generate a migration to create the `waldit` table:
45
+
46
+ ```bash
47
+ rails generate migration create_waldit
48
+ ```
49
+
50
+ And then add the following to your migration file:
51
+
52
+ ```ruby
53
+ class CreateWalditTable < ActiveRecord::Migration[7.0]
54
+ def change
55
+ create_table :waldit do |t|
56
+ t.bigint :transaction_id, null: false
57
+ t.bigint :lsn, null: false
58
+ t.string :action, null: false
59
+ t.jsonb :context, default: {}
60
+ t.string :table_name, null: false
61
+ t.string :primary_key, null: false
62
+ t.jsonb :old, default: {}
63
+ t.jsonb :new, default: {}
64
+ t.timestamp :commited_at
65
+
66
+ t.index [:table_name, :primary_key, :transaction_id], unique: true
67
+ end
68
+ end
69
+ end
70
+ ```
71
+
72
+ 3. **Configure Waldit:**
73
+
74
+ Create an initializer file at `config/initializers/waldit.rb`:
75
+
76
+ ```ruby
77
+ Waldit.configure do |config|
78
+ # A callback that returns true if a table should be watched.
79
+ config.watched_tables = ->(table) { table != "waldit" }
80
+
81
+ # A callback that returns an array of columns to ignore for a given table.
82
+ config.ignored_columns = ->(table) { %w[created_at updated_at] }
83
+ end
84
+ ```
85
+
86
+ 4. **Add context to your changes:**
87
+
88
+ Use the `with_context` method to add context to your database operations:
89
+
90
+ ```ruby
91
+ Waldit.with_context(user_id: 1, reason: "User updated their profile") do
92
+ user.update(name: "New Name")
93
+ end
94
+ ```
95
+
96
+ 5. **Start the watcher:**
97
+
98
+ To process the events, you need to start a WAL watcher. The recommended way is to have a config/waldit.yml
99
+
100
+ ```yml
101
+ slots:
102
+ audit:
103
+ publications: [waldit_publication]
104
+ watcher: Waldit::Watcher
105
+ ```
106
+
107
+ And then run:
108
+
109
+ ```bash
110
+ bundle exec wal start config/waldit.yml
111
+ ```
112
+
113
+ ## How it Works
114
+
115
+ Waldit uses a custom PostgreSQL adapter to set the `waldit_context` session variable before each transaction. This context is then captured by the logical replication slot and stored in the `waldit` table by the `Waldit::Watcher`.
116
+
117
+ The `Waldit::Watcher` is a streaming watcher that listens for changes in the logical replication slot and creates audit records in the `waldit` table. It processes events in batches to minimize the number of database transactions.
@@ -27,6 +27,46 @@ module Waldit
27
27
  end
28
28
  end
29
29
 
30
+ def exec_no_cache(sql, ...)
31
+ return super if READ_QUERY_REGEXP.match? sql
32
+ return super if @current_waldit_context == Waldit.context.hash
33
+
34
+ if transaction_open?
35
+ set_waldit_context!
36
+ super
37
+
38
+ elsif Waldit.context
39
+ # We are trying to execute a query with waldit context while not in a transaction, so we start one
40
+ transaction do
41
+ set_waldit_context!
42
+ super
43
+ end
44
+
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def exec_cache(sql, ...)
51
+ return super if READ_QUERY_REGEXP.match? sql
52
+ return super if @current_waldit_context == Waldit.context.hash
53
+
54
+ if transaction_open?
55
+ set_waldit_context!
56
+ super
57
+
58
+ elsif Waldit.context
59
+ # We are trying to execute a query with waldit context while not in a transaction, so we start one
60
+ transaction do
61
+ set_waldit_context!
62
+ super
63
+ end
64
+
65
+ else
66
+ super
67
+ end
68
+ end
69
+
30
70
  def begin_db_transaction(...)
31
71
  @current_waldit_context = nil.hash
32
72
  super
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module Waldit
5
+ module Sidekiq
6
+ class SaveContext
7
+ include ::Sidekiq::ClientMiddleware
8
+
9
+ def call(job_class, job, queue, redis)
10
+ if (context = Waldit.context)
11
+ job["waldit_context"] = context.to_json
12
+ end
13
+ yield
14
+ end
15
+ end
16
+
17
+ class LoadContext
18
+ include ::Sidekiq::ServerMiddleware
19
+
20
+ def call(job_instance, job, queue, &block)
21
+ context = deserialize_context(job) || {}
22
+ Waldit.with_context(context.merge(background_job: job_instance.class.to_s), &block)
23
+ end
24
+
25
+ private
26
+
27
+ def deserialize_context(job)
28
+ if (serialized_context = job["waldit_context"]) && serialized_context.is_a?(String)
29
+ context = JSON.parse(serialized_context)
30
+ context if context.is_a? Hash
31
+ end
32
+ rescue JSON::ParserError
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end
@@ -2,5 +2,5 @@
2
2
  # typed: true
3
3
 
4
4
  module Waldit
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.3"
6
6
  end
@@ -60,7 +60,7 @@ module Waldit
60
60
  else
61
61
  # Finally the most common case: just deleting a record not created or updated on this transaction
62
62
  record.upsert(
63
- audit.merge(action: "delete", old:),
63
+ audit.merge(action: "delete", old: event.old),
64
64
  unique_by:,
65
65
  on_duplicate: :update,
66
66
  )
data/rbi/waldit.rbi CHANGED
@@ -2,7 +2,7 @@
2
2
  module Waldit
3
3
  extend T::Sig
4
4
  extend Waldit::Context
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.3"
6
6
 
7
7
  class << self
8
8
  sig { returns(String) }
@@ -63,6 +63,39 @@ module Waldit
63
63
  def diff; end
64
64
  end
65
65
 
66
+ module Sidekiq
67
+ class SaveContext
68
+ include ::Sidekiq::ClientMiddleware
69
+
70
+ sig do
71
+ params(
72
+ job_class: T.untyped,
73
+ job: T.untyped,
74
+ queue: T.untyped,
75
+ redis: T.untyped
76
+ ).returns(T.untyped)
77
+ end
78
+ def call(job_class, job, queue, redis); end
79
+ end
80
+
81
+ class LoadContext
82
+ include ::Sidekiq::ServerMiddleware
83
+
84
+ sig do
85
+ params(
86
+ job_instance: T.untyped,
87
+ job: T.untyped,
88
+ queue: T.untyped,
89
+ block: T.untyped
90
+ ).returns(T.untyped)
91
+ end
92
+ def call(job_instance, job, queue, &block); end
93
+
94
+ sig { params(job: T.untyped).returns(T.untyped) }
95
+ def deserialize_context(job); end
96
+ end
97
+ end
98
+
66
99
  class Watcher < Wal::StreamingWatcher
67
100
  extend T::Sig
68
101
 
data/sig/waldit.rbs CHANGED
@@ -39,6 +39,23 @@ module Waldit::Record
39
39
  def diff: () -> ::Hash[String | Symbol, [ untyped, untyped ]]
40
40
  end
41
41
 
42
+ module Waldit::Sidekiq
43
+ end
44
+
45
+ class Waldit::Waldit::Sidekiq::SaveContext
46
+ include ::Sidekiq::ClientMiddleware
47
+
48
+ def call: (untyped job_class, untyped job, untyped queue, untyped redis) -> untyped
49
+ end
50
+
51
+ class Waldit::Waldit::Sidekiq::LoadContext
52
+ include ::Sidekiq::ServerMiddleware
53
+
54
+ def call: (untyped job_instance, untyped job, untyped queue) { () -> untyped } -> untyped
55
+
56
+ def deserialize_context: (untyped job) -> untyped
57
+ end
58
+
42
59
  class Waldit::Watcher < Wal::StreamingWatcher
43
60
  def audit_event: (InsertEvent | UpdateEvent | DeleteEvent event) -> void
44
61
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waldit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Navarro
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-06-29 00:00:00.000000000 Z
10
+ date: 2025-07-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: wal
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sidekiq
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
96
110
  description: Postgres based audit trail for your Active Records, with 100% consistency.
97
111
  email:
98
112
  - rnavarro@rnavarro.com.br
@@ -109,6 +123,7 @@ files:
109
123
  - lib/waldit/postgresql_adapter.rb
110
124
  - lib/waldit/railtie.rb
111
125
  - lib/waldit/record.rb
126
+ - lib/waldit/sidekiq.rb
112
127
  - lib/waldit/version.rb
113
128
  - lib/waldit/watcher.rb
114
129
  - rbi/waldit.rbi