Skip to content

Commit

Permalink
Merge pull request #5 from VegetableProphet/refactoring
Browse files Browse the repository at this point in the history
Heavy refactoring
  • Loading branch information
0exp authored Feb 26, 2020
2 parents 98c778c + b164845 commit 720e26f
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 260 deletions.
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
inherit_gem:
rubocop-config-umbrellio: lib/rubocop.yml

AllCops:
TargetRubyVersion: 2.4.7

Naming/FileName:
Exclude:
- lib/sequel-bulk-audit.rb
9 changes: 7 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
sudo: false
language: ruby
rvm:
- 2.4.1
before_install: gem install bundler -v 1.14.6
- 2.4.7
- 2.5.7
- 2.6.5
- 2.7.0
- ruby-head

before_install: gem install bundler -v 2.1.4
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

### Added

- integration with rubocop ecosystem

### 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.
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in sequel-bulk-audit.gemspec
gemspec
57 changes: 40 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,58 @@ 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
```

Correct usage for several models in one transaction:

```ruby
DB.transaction do
Model.with_current_user(current_user) do # will create temp table for model
Model.where(...).update(...)
end

OtherModel.with_current_user(current_user) do # will create temp table for other_model
OtherModel.where(...).update(...)
end
end
```

Incorrect usage:

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

## Migration from 0.2.0 to 1.0.0

Recreate audit_changes() function with new changes.

## 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 +80,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.
4 changes: 3 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

task :default => :spec
task default: :spec
6 changes: 4 additions & 2 deletions lib/generators/audit_migration/audit_migration_generator.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# frozen_string_literal: true

class AuditMigrationGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
source_root File.expand_path("templates", __dir__)

def copy_audit_migration_files
version = Time.now.utc.strftime('%Y%m%d%H%M%S')
version = Time.now.utc.strftime("%Y%m%d%H%M%S")
copy_file "01_migration.rb", "db/migrate/#{version}_CreateAuditTableAndTrigger.rb"
end
end
33 changes: 16 additions & 17 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,8 +71,9 @@
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,
__audit_info.username, current_query(), __audit_info.data);
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;
SQL
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,44 +1,51 @@
# frozen_string_literal: true

require "sequel/plugins/bulk_audit/version"
require 'sequel/model'
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
Expand Down
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
28 changes: 15 additions & 13 deletions sequel-bulk-audit.gemspec
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# coding: utf-8
lib = File.expand_path("../lib", __FILE__)
# frozen_string_literal: true

lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "sequel/plugins/bulk_audit/version"

Expand All @@ -9,28 +11,28 @@ Gem::Specification.new do |spec|
spec.authors = ["Fox"]
spec.email = ["[email protected]"]

spec.summary = %q{This gem provides a trigger based solution for auditing table changes}
spec.description = %q{Every update on audited table will be logged. You can update the table in bulk}
spec.homepage = "https://github.com/fiscal-cliff/sequel-bulk-audit/"
spec.post_install_message = %q{ Next steps:
spec.summary = "This gem provides a trigger based solution for auditing table changes"
spec.description = "Every update on audited table will be logged. You can update in bulk"
spec.homepage = "https://github.com/umbrellio/sequel-bulk-audit/"
spec.post_install_message = ' Next steps:
1. Run rails g audit_migration
2. Edit generated migration
3. Apply the migration"
}
'

spec.files = `git ls-files -z`.split("\x0").reject do |f|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/})
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "pg", ">= 0.17.0"
spec.add_dependency "sequel", ">= 4.0.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 "pry", "~> 0.10"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rubocop-config-umbrellio"
end
Loading

0 comments on commit 720e26f

Please sign in to comment.