diff --git a/README.md b/README.md index 7a076c2c..1a03c203 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,22 @@ In 4.10, the default behavior for enums changed from storing the value synthesiz Audited.store_synthesized_enums = true ``` +### Audit SQL + +To fetch the SQL used to create the audit, you can use the `audit_sql` method. Can be useful for batched operations or for debugging. + +```ruby +class User < ActiveRecord::Base + audited +end + +user = User.new(name: "Brandon") +user.disable_auditing +user.audit_sql +``` + +NOTE: This method will return non-nil value only if the record is dirty (is new or has changes). + ## Support You can find documentation at: https://www.rubydoc.info/gems/audited diff --git a/lib/audited/auditor.rb b/lib/audited/auditor.rb index a164f72a..70c61799 100644 --- a/lib/audited/auditor.rb +++ b/lib/audited/auditor.rb @@ -218,6 +218,44 @@ def combine_audits(audits_to_combine) end end + def audit_sql(destroy: false) + return unless changed? || destroy + + action = if new_record? + "create" + elsif destroy + "destroy" + else + "update" + end + + attrs = { + action: action, + audited_changes: audited_changes, + comment: audit_comment + } + attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil? + changes = run_callbacks(:audit) do + audit = audits.new(attrs) + audit.run_callbacks(:create) + result = audit.changes + audits.delete(audit) + result + end + changes = changes.each_with_object({}) do |(k, v), h| + h[k] = v.last + h[k] = h[k].to_json if h[k].is_a?(Hash) + end + changes["created_at"] ||= Time.current + + stmt = Arel::InsertManager.new + table = Arel::Table.new(Audited.audit_class.table_name) + stmt.into(table) + changes.keys.each { |key| stmt.columns << table[key] } + stmt.values = stmt.create_values(changes.values) + stmt.to_sql + end + protected def revision_with(attributes) diff --git a/spec/audited/auditor_spec.rb b/spec/audited/auditor_spec.rb index 15ff2f77..36914156 100644 --- a/spec/audited/auditor_spec.rb +++ b/spec/audited/auditor_spec.rb @@ -437,6 +437,50 @@ class CallbacksSpecified < ::ActiveRecord::Base expect { @user.update_attribute :activated, "1" }.to_not change(Audited::Audit, :count) end + it "should return sql for audit when changes are made" do + expect(@user.new_record?).to eq(false) + expect(@user.changes).to be_blank + expect(@user.audit_sql).to eq(nil) + + @user.assign_attributes(name: "Changed") + audit_sql = @user.audit_sql + + def parse_sql(sql) + matches = sql.match(/INSERT INTO "audits" \((.*?)\) VALUES \((.*?)\)/) + columns = matches[1].split(", ").map { |c| c.delete('"') } + values = matches[2].split(", ").map { |v| v.delete("'") } + columns.zip(values).to_h + end + parsed_sql = parse_sql(audit_sql) + # expect(parsed_sql["auditable_id"]).to eq("1") + expect(parsed_sql["auditable_type"]).to eq("Models::ActiveRecord::User") + expect(parsed_sql["action"]).to eq("update") + expect(parsed_sql["audited_changes"]).to eq('{"name":["Brandon","Changed"]}') + expect(parsed_sql["version"]).to eq("2") + + @user.save! + expect(@user.audit_sql).to eq(nil) + + last_audit = @user.audits.last + expect(last_audit.action).to eq("update") + expect(last_audit.audited_changes).to eq({"name" => ["Brandon", "Changed"]}) + expect(last_audit.version).to eq(2) + + @user.assign_attributes(name: "Changed-2") + expect { ActiveRecord::Base.connection.execute(@user.audit_sql) }.to change(@user.audits, :count).by(1) + expect { @user.class.where(id: @user.id).update_all(name: "Changed-2") }.not_to change(@user.audits, :count) + expect { @user.reload.save! }.not_to change(@user.audits, :count) + + expect { @user.update!(name: "Changed-3") }.to change(@user.audits, :count).by(1) + expect(@user.audits.last.version).to eq(4) + + destroy_sql = parse_sql(@user.audit_sql(destroy: true)) + expect(destroy_sql["action"]).to eq("destroy") + + @user.assign_attributes(name: "Changed-4") + expect { @user.audit_sql }.not_to change(@user.audits, :count) + end + context "with readonly attributes" do before do @user = create_user_with_readonly_attrs(status: "active")