diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a5942..e11abff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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][]) @@ -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 diff --git a/README.md b/README.md index aebac11..813512e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: diff --git a/lib/logidze/meta.rb b/lib/logidze/meta.rb index 20772dc..6a2a82e 100644 --- a/lib/logidze/meta.rb +++ b/lib/logidze/meta.rb @@ -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 @@ -38,8 +88,10 @@ 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 @@ -47,27 +99,31 @@ def call_block_in_meta_context 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 @@ -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 diff --git a/spec/integration/meta_spec.rb b/spec/integration/meta_spec.rb index 3d3da2a..7269a53 100644 --- a/spec/integration/meta_spec.rb +++ b/spec/integration/meta_spec.rb @@ -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