Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
hamster committed Feb 18, 2020
1 parent 98c778c commit 31e17fa
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 241 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ sudo: false
language: ruby
rvm:
- 2.4.1
before_install: gem install bundler -v 1.14.6
- 2.6.5
before_install: gem install bundler -v 2.0.0
addons:
postgresql: "9.6"
services:
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Change Log
All notable changes to this project will be documented in this file.

## [1.0.0] - 2020-02-18

### Migration from 0.2.0 to 1.0.0

Recreate audit_changes() function with new changes.

### Changed

- instead of creating model_to_table_map temp table now keeps just model_name
- temp table now drops on commit
- temp table now named as "__schema_table_audit_logs_trid"
- temp table now has array of audited table columns
- trigger function now uses array of columns from temp table instead of querying for them
- remade specs
- readme
- isolated tests
- incapsulated preparations for tests in SeedHelper

### Removed

- redundant self
- redundant excluded columns option
- ability to use #with_current_user on instances of audited class
- spec for polymorhic associations

## [0.2.0] - 2018-06-08

Initial version.
39 changes: 22 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
# sequel-bulk-audit [![Build Status](https://travis-ci.org/fiscal-cliff/sequel-bulk-audit.svg?branch=master)](https://travis-ci.org/fiscal-cliff/sequel-bulk-audit)
# sequel-bulk-audit [![Build Status](https://travis-ci.org/umbrellio/sequel-bulk-audit.svg?branch=master)](https://travis-ci.org/umbrellio/sequel-bulk-audit)

This gem allows you to track any changes in your tables. This approach not only is suitable for model updates but also enables you to track dataset updates.
This gem allows you to track any changes in your tables. This approach is not only suitable for model updates but also enables you to track dataset updates.

You should wrap your updating code as follows:

```ruby
Model.with_current_user(current_user) do
Model.where(...).update(...)
end
```

Method #with_current_user expects current_user to be an object (or record) having attributes id and login

You are able setup polymorphic associations between audit records and corresponding records.
Method #with_current_user expects current_user to be an object (or record) having attributes id and login. It sets user_id as 0 and login as "unspecified" by default.

## Installation

Expand All @@ -30,25 +20,40 @@ Or install it yourself as:

$ gem install sequel-bulk-audit

After Installation you should run ```rails g audit_migration``` generator.
After installation you should run ```rails g audit_migration``` generator.

You can exdend this migration by attaching the trigger to audited tables.

Please note, that this gem reqires pg_array and pg_json sequel extensions to work.

## Usage

Models, changes in which you plan to audit should contain
Models with audited changes should contain:

```ruby
plugin :bulk_audit
```

Method #with_current_user should wrap all the operations on the table.
Method #with_current_user should wrap all the operations on the table. You must use method from the model you are changing for this gem to work correclty.

Keep in mind that everything wraped in #with_current_user will happen in one transaction.

Correct usage:

```ruby
Model.with_current_user(current_user) do
Model.where(...).update(...)
end
```

Incorrect usage:

```ruby
SomeOtherModel.with_current_user(current_user) do
Model.where(...).update(...)
end
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand All @@ -57,4 +62,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/fiscal-cliff/sequel-bulk-audit.
Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/sequel-bulk-audit.
30 changes: 14 additions & 16 deletions lib/generators/audit_migration/templates/01_migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
create_function(:audit_changes, <<~SQL, returns: :trigger, language: :plpgsql, replace: true)
DECLARE
changes jsonb := '{}'::jsonb;
ri RECORD;
column_name text;
n jsonb;
o jsonb;
__audit_info RECORD;
Expand All @@ -32,28 +32,26 @@
trid bigint;
BEGIN
SELECT txid_current() INTO trid;
EXECUTE 'SELECT * FROM __audit_info_' || trid::text INTO __audit_info;
FOR ri IN
SELECT column_name
FROM information_schema.columns
WHERE
table_schema = quote_ident(TG_TABLE_SCHEMA)
AND table_name = quote_ident(TG_TABLE_NAME)
ORDER BY ordinal_position
EXECUTE CONCAT(
'SELECT * FROM __', TG_TABLE_SCHEMA, '_', TG_TABLE_NAME, '_audit_info_', trid::text
) INTO __audit_info;
FOREACH column_name IN ARRAY __audit_info.columns
LOOP
IF (TG_OP = 'UPDATE') THEN
EXECUTE 'SELECT to_jsonb(($1).' || ri.column_name || ')' INTO n USING NEW;
EXECUTE 'SELECT to_jsonb(($1).' || ri.column_name || ')' INTO o USING OLD;
EXECUTE 'SELECT to_jsonb(($1).' || column_name || ')' INTO n USING NEW;
EXECUTE 'SELECT to_jsonb(($1).' || column_name || ')' INTO o USING OLD;
IF (o != n) THEN
SELECT changes || jsonb_build_object(ri.column_name, ARRAY[o, n]) INTO changes;
SELECT changes || jsonb_build_object(column_name, ARRAY[o, n]) INTO changes;
END IF;
ELSE
IF (TG_OP = 'DELETE') THEN
EXECUTE 'SELECT to_jsonb(($1).' || ri.column_name || ')' INTO n USING OLD;
EXECUTE 'SELECT to_jsonb(($1).' || column_name || ')' INTO n USING OLD;
ELSIF (TG_OP = 'INSERT') THEN
EXECUTE 'SELECT to_jsonb(($1).' || ri.column_name || ')' INTO n USING NEW;
EXECUTE 'SELECT to_jsonb(($1).' || column_name || ')' INTO n USING NEW;
END IF;
SELECT changes || jsonb_build_object(ri.column_name, n) INTO changes;
SELECT changes || jsonb_build_object(column_name, n) INTO changes;
END IF;
END LOOP;
Expand All @@ -73,7 +71,7 @@
END CASE;
INSERT INTO audit_logs ("model_type", "model_id", "event", "changed",
"created_at", "user_id", "username", "query", "data")
VALUES (coalesce((__audit_info.model_map ->> TG_TABLE_NAME::TEXT), TG_TABLE_NAME::TEXT), model_id, TG_OP, changes, NOW(), __audit_info.user_id,
VALUES (coalesce(__audit_info.model_name::TEXT, TG_TABLE_NAME::TEXT), model_id, TG_OP, changes, NOW(), __audit_info.user_id,
__audit_info.username, current_query(), __audit_info.data);
RETURN return_record;
END;
Expand Down
2 changes: 2 additions & 0 deletions lib/sequel-bulk-audit.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# frozen_string_literal: true

require "pry"
require "sequel/plugins/bulk_audit"
65 changes: 36 additions & 29 deletions lib/sequel/plugins/bulk_audit.rb
Original file line number Diff line number Diff line change
@@ -1,45 +1,52 @@
# frozen_string_literal: true

require "sequel/plugins/bulk_audit/version"
require 'sequel/model'

module Sequel
module Plugins
module BulkAudit
def self.apply(model, opts={})
model.instance_eval do
@excluded_columns = [*opts[:excluded_columns]]
module ClassMethods
def with_current_user(current_user, attributes = {})
db.transaction do
data = db.select(
Sequel.expr(current_user&.id || 0).as(:user_id),
Sequel.cast(current_user&.login || "unspecified", :text).as(:username),
Sequel.expr(name).as(:model_name),
Sequel.pg_array(stringified_columns).as(:columns),
Sequel.pg_jsonb(attributes).as(:data),
)

create_temp_table(data)

yield if block_given?
end
end
end

module SharedMethods
def model_to_table_map
@@model_to_table_map ||= ObjectSpace.each_object(Class).select do |klazz|
next if klazz.name.nil?
klazz < Sequel::Model && klazz&.plugins&.include?(Sequel::Plugins::BulkAudit)
end.map { |c| [c.to_s, c.table_name] }.to_h.invert
def trid
db.get(Sequel.function(:txid_current))
end

def with_current_user(current_user, attributes = nil)
self.db.transaction do
trid = self.db.select(Sequel.function(:txid_current)).single_value
data = self.db.select(Sequel.expr(current_user&.id || 0).as(:user_id),
Sequel.cast(current_user&.login || "unspecified", :text).as(:username),
Sequel.pg_jsonb(model_to_table_map).as(:model_map),
Sequel.pg_jsonb(attributes || {}).as(:data))
self.db.create_table!(:"__audit_info_#{trid}", temp: true, as: data)
result = yield if block_given?
self.db.drop_table?(:"__audit_info_#{trid}")
result
end
def create_temp_table(data)
db.create_table!(audit_logs_temp_table_name, on_commit: :drop, temp: true, as: data)
end
end

module ClassMethods
include SharedMethods
end
def stringified_columns
columns.map(&:to_s)
end

module InstanceMethods
include SharedMethods
# uses trid so temp table would be unique between transactions
# uses table_name so temp table would be unique if several models are audited at once
def audit_logs_temp_table_name
"__#{table_name_with_schema}_audit_info_#{trid}".to_sym
end

def table_name_with_schema
return "public_#{table_name}" if table_name.is_a?(Symbol)

"#{table_name.table}_#{table_name.column}" # for QualifiedIdentifier
end
end
end
end
end
end
4 changes: 3 additions & 1 deletion lib/sequel/plugins/bulk_audit/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

module Sequel
module Plugins
module BulkAudit
VERSION = "0.2.0"
VERSION = "1.0.0"
end
end
end
11 changes: 5 additions & 6 deletions sequel-bulk-audit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_dependency "sequel", ">= 4.0.0"
spec.add_dependency "pg", ">= 0.17.0"
spec.add_dependency "pg", ">= 0.17.0"

spec.add_development_dependency "bundler", "~> 1.14"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "pry", "~> 0.10"
spec.add_development_dependency "sequel_polymorphic"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "pry", "~> 0.10"
end
6 changes: 0 additions & 6 deletions spec/fixtures/data.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
- id: 1
created_at: '2017-04-01'
value: 1
- id: 2
created_at: '2017-04-01'
value: 2
- id: 3
created_at: '2017-05-01'
value: 3
- id: 4
created_at: '2017-05-01'
value: 4
- id: 5
created_at: '2017-05-01'
value: 5
- id: 6
created_at: '2017-05-01'
value: 6
48 changes: 48 additions & 0 deletions spec/seed_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module SeedHelper
class << self
def prepare_data_table
drop_data
create_data
seed_data
restart_data_seq
create_trigger_on_data
end

def create_data
DB.create_table(:data) do
primary_key :id
String :value
end
end

def seed_data
data = YAML.load(IO.read("spec/fixtures/data.yml"))
DB[:data].multi_insert(data)
end

def restart_data_seq
id = DB[:data].max(:id) + 1

DB.execute(<<-SQL)
ALTER SEQUENCE data_id_seq RESTART WITH #{id};
SQL
end

def create_trigger_on_data
DB.run <<~SQL
CREATE TRIGGER audit_changes_on_data BEFORE INSERT OR UPDATE OR DELETE ON data
FOR EACH ROW EXECUTE PROCEDURE audit_changes();
SQL
end

def drop_data
DB.drop_table?(:data)
end

def clear_audit_logs
DB.tables.include?(:audit_logs) && DB[:audit_logs].truncate
end
end
end
Loading

0 comments on commit 31e17fa

Please sign in to comment.