Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce auditable.audit_sql for fetching next audit insert query #737

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 44 additions & 0 deletions spec/audited/auditor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down