Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## master (unreleased)

- Add block-less versions of `with_responsible` and `with_metata`. ([@atomaka][])

## 1.4.1 (2025-06-05)

- Don't drop functions in the upgrade migration. ([@palkan][])
Expand Down Expand Up @@ -416,3 +418,4 @@ This is a quick fix for a more general problem (see [#59](https://github.com/pal
[@SparLaimor]: https://github.com/SparLaimor
[@tagirahmad]: https://github.com/tagirahmad
[@tylerhunt]: https://github.com/tylerhunt
[@atomaka]: https://github.com/atomaka
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Other requirements:
- [Basic API](#basic-api)
- [Track meta information](#track-meta-information)
- [Track responsibility](#track-responsibility)
- [Persisted metadata](#persisted-metadata)
- [Disable logging temporary](#disable-logging-temporary)
- [Reset log](#reset-log)
- [Creating full snapshot instead of diffs](#full-snapshots)
Expand Down Expand Up @@ -410,6 +411,26 @@ Logidze.with_responsible(user.id, transactional: false) do
end
```

#### Persisted metadata

You can also set metadata and responsibility that persists for the database connection using the bang versions of these methods:

```ruby
Logidze.with_meta!({ip: request.ip})
post.save!

Logidze.with_responsible!(user.id)
post.save!
```

This persisted information needs to be explicitly cleared.

```ruby
Logidze.clear_meta!
```

**Important:** Persisted metadata is set at the connection level and will affect all subsequent operations on that connection until cleared. Always ensure you call `clear_meta!` when done, especially in web applications where connections are reused.

### Disable logging temporary

If you want to make update without logging (e.g., mass update), you can turn it off the following way:
Expand Down
94 changes: 76 additions & 18 deletions lib/logidze/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,67 @@ def with_responsible(responsible_id, transactional: true, &block)
with_meta(meta, transactional: transactional, &block)
end

class MetaWrapper # :nodoc:
def self.wrap_with(meta, &block)
new(meta, &block).perform
def with_meta!(meta)
return if meta.nil?

if Thread.current[:logidze_in_block]
raise StandardError, "with_meta! cannot be called from within a with_meta block"
end

attr_reader :meta, :block
MetaForConnection.new(meta).set!
end

def clear_meta!
MetaForConnection.new({}).clear!
end

def with_responsible!(responsible_id)
return if responsible_id.nil?

meta = {Logidze::History::Version::META_RESPONSIBLE => responsible_id}
with_meta!(meta)
end

class MetaBase # :nodoc:
attr_reader :meta

delegate :connection, to: ActiveRecord::Base

def initialize(meta, &block)
def initialize(meta)
@meta = meta
end

def current_meta
meta_stack.reduce(:merge) || {}
end

def meta_stack
Thread.current[:meta] ||= []
Thread.current[:meta]
end

def encode_meta(value)
connection.quote(ActiveSupport::JSON.encode(value))
end

def pg_reset_meta_param(prev_meta)
if prev_meta.empty?
pg_clear_meta_param
else
pg_set_meta_param(prev_meta)
end
end
end

class MetaWrapper < MetaBase # :nodoc:
def self.wrap_with(meta, &block)
new(meta, &block).perform
end

attr_reader :block

def initialize(meta, &block)
super(meta)
@block = block
end

Expand All @@ -38,36 +88,42 @@ def perform

def call_block_in_meta_context
prev_meta = current_meta
was_in_block = Thread.current[:logidze_in_block]

meta_stack.push(meta)
Thread.current[:logidze_in_block] = true

pg_set_meta_param(current_meta)
result = block.call
result
ensure
pg_reset_meta_param(prev_meta)
meta_stack.pop
Thread.current[:logidze_in_block] = was_in_block
end
end

def current_meta
meta_stack.reduce(:merge) || {}
class MetaForConnection < MetaBase # :nodoc:
def set!
return if meta.nil?

meta_stack.push(meta)
pg_set_meta_param(current_meta)
end

def meta_stack
Thread.current[:meta] ||= []
Thread.current[:meta]
def clear!
meta_stack.clear
pg_clear_meta_param
end

def encode_meta(value)
connection.quote(ActiveSupport::JSON.encode(value))
private

def pg_set_meta_param(value)
connection.execute("SET logidze.meta = #{encode_meta(value)};")
end

def pg_reset_meta_param(prev_meta)
if prev_meta.empty?
pg_clear_meta_param
else
pg_set_meta_param(prev_meta)
end
def pg_clear_meta_param
connection.execute("SET logidze.meta TO DEFAULT;")
end
end

Expand Down Expand Up @@ -99,7 +155,9 @@ def pg_clear_meta_param
end
end

private_constant :MetaBase
private_constant :MetaWrapper
private_constant :MetaForConnection
private_constant :MetaWithTransaction
private_constant :MetaWithoutTransaction
end
Expand Down
84 changes: 84 additions & 0 deletions spec/integration/meta_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,88 @@
end
end
end

describe ".with_meta!" do
subject { User.create!(name: "test", age: 10, active: false) }

after { Logidze.clear_meta! }

context "setting meta for connection" do
it "sets meta for connection and persists across operations" do
Logidze.with_meta!(meta)

expect(subject.reload.meta).to eq(meta)
end

it "handles nil" do
Logidze.with_meta!(nil)

expect(subject.reload.meta).to be_nil
expect(subject.log_data.current_version.data.keys).not_to include(Logidze::History::Version::META)
end

it "cannot be called inside block version" do
Logidze.with_meta(meta) do
expect { Logidze.with_meta!(meta2) }.to raise_error(StandardError, /cannot be called from within a with_meta block/)
end
end
end
end

describe ".with_responsible!" do
let(:responsible) { User.create!(name: "owner") }

subject { User.create!(name: "test", age: 10, active: false) }

after { Logidze.clear_meta! }

context "setting responsible for connection" do
it "sets responsible for connection and persists across operations" do
Logidze.with_responsible!(responsible.id)

expect(subject.reload.whodunnit).to eq(responsible)

subject.update!(age: 11)
expect(subject.reload.whodunnit).to eq(responsible)
end

it "handles nil responsible_id" do
Logidze.with_responsible!(nil)

expect(subject.reload.whodunnit).to be_nil
end

it "can be cleared with clear_meta!" do
Logidze.with_responsible!(responsible.id)

expect(subject.reload.whodunnit).to eq(responsible)

Logidze.clear_meta!

subject.update!(age: 12)
expect(subject.reload.whodunnit).to be_nil
end

it "can be changed to a different responsible" do
responsible2 = User.create!(name: "owner2")

Logidze.with_responsible!(responsible.id)
expect(subject.reload.whodunnit).to eq(responsible)

Logidze.with_responsible!(responsible2.id)
subject.update!(age: 11)
expect(subject.reload.whodunnit).to eq(responsible2)
end

it "can add to existing metadata" do
Logidze.with_meta!(meta)

Logidze.with_responsible!(responsible.id)
expect(subject.reload.whodunnit).to eq(responsible)
expect(subject.meta).to eq(meta.merge(
Logidze::History::Version::META_RESPONSIBLE => responsible.id
))
end
end
end
end
Loading