breathing 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +10 -3
- data/README.md +12 -9
- data/breathing.gemspec +4 -2
- data/lib/breathing.rb +1 -0
- data/lib/breathing/cli.rb +1 -1
- data/lib/breathing/installer.rb +9 -0
- data/lib/breathing/trigger.rb +41 -45
- data/spec/app.rb +1 -1
- data/spec/breathing/excel_spec.rb +3 -1
- data/spec/breathing_spec.rb +36 -31
- data/spec/database.yml +10 -3
- data/spec/spec_helper.rb +2 -0
- metadata +36 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62878c327e1da216456749f0d3d3b154a4d8d11528226f90cc7dadf855d2e6f2
|
4
|
+
data.tar.gz: 7a27d00aca80c0a3837d1bfc7778ee1d507c2deb7719127a53745a86f25aa439
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86f14f92c0a6b4953307c6a733daf0c378726f0e3d9f97703c29ff3e34ead58568b4b3d0d62ddd2cd34da40b6d1056cc6a6e1b096030276d6313745844a25998
|
7
|
+
data.tar.gz: 51cb295e2097b87d2ba21faf62495087a5cae5a7db695e7703e219cbde57f883b4f776d39ca68258299ba6f33347c1bca3ce1608580b27f5a2e251f978c3c86f
|
data/.circleci/config.yml
CHANGED
@@ -6,8 +6,7 @@ executors:
|
|
6
6
|
docker:
|
7
7
|
- image: circleci/ruby:2.6
|
8
8
|
environment:
|
9
|
-
DB_USER:
|
10
|
-
DB_PASS: 'root'
|
9
|
+
DB_USER: root
|
11
10
|
DB_HOST: '127.0.0.1'
|
12
11
|
- image: circleci/mysql:8-ram
|
13
12
|
environment:
|
@@ -15,6 +14,10 @@ executors:
|
|
15
14
|
MYSQL_ROOT_PASSWORD: root
|
16
15
|
MYSQL_DATABASE: breathing_test
|
17
16
|
command: [--default-authentication-plugin=mysql_native_password]
|
17
|
+
- image: circleci/postgres:10.6-alpine-ram
|
18
|
+
environment:
|
19
|
+
POSTGRES_USER: root
|
20
|
+
POSTGRES_DB: breathing_test
|
18
21
|
|
19
22
|
commands:
|
20
23
|
setup_bundle:
|
@@ -35,6 +38,9 @@ commands:
|
|
35
38
|
- run:
|
36
39
|
name: Wait for DB
|
37
40
|
command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m
|
41
|
+
- run:
|
42
|
+
name: Wait for DB
|
43
|
+
command: dockerize -wait tcp://127.0.0.1:5432 -timeout 1m
|
38
44
|
|
39
45
|
jobs:
|
40
46
|
test:
|
@@ -43,7 +49,8 @@ jobs:
|
|
43
49
|
- checkout
|
44
50
|
- setup_bundle
|
45
51
|
- wait_for_db
|
46
|
-
- run: bundle exec rspec ./spec
|
52
|
+
- run: DB=mysql DB_PASS=root bundle exec rspec ./spec
|
53
|
+
- run: DB=pg bundle exec rspec ./spec
|
47
54
|
|
48
55
|
workflows:
|
49
56
|
version: 2
|
data/README.md
CHANGED
@@ -5,15 +5,8 @@ Logging mechanism using database triggers to store the old and new row states in
|
|
5
5
|
|
6
6
|
## Install
|
7
7
|
|
8
|
-
Put this line in your Gemfile:
|
9
|
-
|
10
|
-
```
|
11
|
-
gem 'breathing'
|
12
|
-
```
|
13
|
-
|
14
|
-
Then bundle:
|
15
8
|
```
|
16
|
-
|
9
|
+
gem install 'breathing'
|
17
10
|
```
|
18
11
|
|
19
12
|
## Usage
|
@@ -24,6 +17,8 @@ Just run the following command.
|
|
24
17
|
|
25
18
|
```
|
26
19
|
% DATABASE_URL="mysql2://user:pass@host:port/database" breathing install
|
20
|
+
or
|
21
|
+
% DATABASE_URL="postgres://user:pass@host:port/database" breathing install
|
27
22
|
```
|
28
23
|
|
29
24
|
- Create table `change_logs`
|
@@ -46,12 +41,20 @@ Cleanup command.
|
|
46
41
|
- change_logs_update_{table_name}
|
47
42
|
- change_logs_delete_{table_name}
|
48
43
|
|
49
|
-
###
|
44
|
+
### Export
|
50
45
|
|
51
46
|
```
|
52
47
|
% DATABASE_URL="mysql2://user:pass@host:port/database" breathing export
|
53
48
|
```
|
54
49
|
|
50
|
+
- Output file `breathing.xlsx`
|
51
|
+
|
52
|
+
## Compatibility
|
53
|
+
|
54
|
+
- Ruby 2.3.0+
|
55
|
+
- MySQL 5.7.0+
|
56
|
+
- PostgreSQL 8.0+
|
57
|
+
|
55
58
|
## Copyright
|
56
59
|
|
57
60
|
Copyright (c) 2020 Akira Kusumoto. See MIT-LICENSE file for further details.
|
data/breathing.gemspec
CHANGED
@@ -2,7 +2,7 @@ $:.push File.expand_path('lib', __dir__)
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'breathing'
|
5
|
-
s.version = '0.0.
|
5
|
+
s.version = '0.0.4'
|
6
6
|
s.platform = Gem::Platform::RUBY
|
7
7
|
s.authors = ['Akira Kusumoto']
|
8
8
|
s.email = ['akirakusumo10@gmail.com']
|
@@ -21,8 +21,10 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.add_runtime_dependency 'thor'
|
22
22
|
|
23
23
|
s.add_dependency 'activerecord', ['>= 5.0.0']
|
24
|
-
s.add_dependency '
|
24
|
+
s.add_dependency 'hairtrigger'
|
25
25
|
s.add_dependency 'mysql2'
|
26
|
+
s.add_dependency 'pg'
|
27
|
+
s.add_dependency 'rubyXL', ['>= 3.4.0']
|
26
28
|
s.add_development_dependency 'pry-byebug'
|
27
29
|
s.add_development_dependency 'rspec', '~> 3.9'
|
28
30
|
end
|
data/lib/breathing.rb
CHANGED
data/lib/breathing/cli.rb
CHANGED
data/lib/breathing/installer.rb
CHANGED
@@ -4,8 +4,12 @@ require 'breathing/trigger'
|
|
4
4
|
require 'breathing/change_log'
|
5
5
|
|
6
6
|
module Breathing
|
7
|
+
class UnsupportedError < StandardError; end
|
8
|
+
|
7
9
|
class Installer
|
8
10
|
def install
|
11
|
+
raise Breathing::UnsupportedError, "Version MySQL 5.6 is not supported." unless database_version_valid?
|
12
|
+
|
9
13
|
create_log_table unless log_table_exists?
|
10
14
|
|
11
15
|
models.each do |model|
|
@@ -24,6 +28,11 @@ module Breathing
|
|
24
28
|
|
25
29
|
private
|
26
30
|
|
31
|
+
def database_version_valid?
|
32
|
+
connection = ActiveRecord::Base.connection
|
33
|
+
connection.adapter_name == "PostgreSQL" || (connection.adapter_name == 'Mysql2' && connection.raw_connection.info[:version].to_f >= 5.7)
|
34
|
+
end
|
35
|
+
|
27
36
|
def log_table_name
|
28
37
|
Breathing::ChangeLog.table_name
|
29
38
|
end
|
data/lib/breathing/trigger.rb
CHANGED
@@ -12,69 +12,61 @@ module Breathing
|
|
12
12
|
|
13
13
|
def create
|
14
14
|
trigger_name = "#{log_table_name}_insert_#{model.table_name}"
|
15
|
+
|
15
16
|
unless exists?(trigger_name)
|
16
17
|
puts "CREATE TRIGGER #{trigger_name}"
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
FOR EACH ROW
|
22
|
-
BEGIN
|
23
|
-
INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
|
18
|
+
|
19
|
+
ActiveRecord::Base.connection.create_trigger(trigger_name).on(model.table_name).after(:insert) do
|
20
|
+
<<-SQL
|
21
|
+
INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
|
24
22
|
VALUES ('INSERT', '#{model.table_name}', NEW.id,
|
25
|
-
|
26
|
-
|
23
|
+
'{}',
|
24
|
+
#{row_to_json(model.columns, 'NEW')},
|
27
25
|
CURRENT_TIMESTAMP);
|
28
|
-
|
29
|
-
|
26
|
+
SQL
|
27
|
+
end
|
30
28
|
end
|
31
29
|
|
32
30
|
trigger_name = "#{log_table_name}_update_#{model.table_name}"
|
33
31
|
unless exists?(trigger_name)
|
34
32
|
puts "CREATE TRIGGER #{trigger_name}"
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
FOR EACH ROW
|
40
|
-
BEGIN
|
41
|
-
IF (OLD.updated_at != NEW.updated_at) THEN
|
42
|
-
INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
|
33
|
+
|
34
|
+
ActiveRecord::Base.connection.create_trigger(trigger_name).on(model.table_name).before(:update).of(:updated_at) do
|
35
|
+
<<-SQL
|
36
|
+
INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
|
43
37
|
VALUES ('UPDATE', '#{model.table_name}', NEW.id,
|
44
|
-
|
45
|
-
|
38
|
+
#{row_to_json(model.columns, 'OLD')},
|
39
|
+
#{row_to_json(model.columns, 'NEW')},
|
46
40
|
CURRENT_TIMESTAMP);
|
47
|
-
|
48
|
-
|
49
|
-
SQL
|
41
|
+
SQL
|
42
|
+
end
|
50
43
|
end
|
51
44
|
|
52
45
|
trigger_name = "#{log_table_name}_delete_#{model.table_name}"
|
53
46
|
unless exists?(trigger_name)
|
54
47
|
puts "CREATE TRIGGER #{trigger_name}"
|
55
|
-
ActiveRecord::Base.connection.
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
|
62
|
-
VALUES ('DELETE', '#{model.table_name}', OLD.id,
|
63
|
-
JSON_OBJECT(#{json_object_values(model.columns, 'OLD')}),
|
64
|
-
JSON_OBJECT(),
|
48
|
+
ActiveRecord::Base.connection.create_trigger(trigger_name).on(model.table_name).after(:delete) do
|
49
|
+
<<-SQL
|
50
|
+
INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
|
51
|
+
VALUES ('DELETE', '#{model.table_name}', OLD.id,
|
52
|
+
#{row_to_json(model.columns, 'OLD')},
|
53
|
+
'{}',
|
65
54
|
CURRENT_TIMESTAMP);
|
66
|
-
|
67
|
-
|
55
|
+
SQL
|
56
|
+
end
|
68
57
|
end
|
69
58
|
end
|
70
59
|
|
71
60
|
def drop
|
72
61
|
%w[insert update delete].each do |action|
|
73
62
|
trigger_name = "#{log_table_name}_#{action}_#{model.table_name}"
|
74
|
-
next unless exists?(trigger_name)
|
75
63
|
|
76
64
|
begin
|
77
|
-
sql = "DROP TRIGGER #{trigger_name}"
|
65
|
+
sql = "DROP TRIGGER IF EXISTS #{trigger_name}"
|
66
|
+
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
67
|
+
sql << " ON #{model.table_name} CASCADE;"
|
68
|
+
sql << " DROP FUNCTION IF EXISTS #{trigger_name} CASCADE;"
|
69
|
+
end
|
78
70
|
puts sql
|
79
71
|
ActiveRecord::Base.connection.execute(sql)
|
80
72
|
rescue StandardError => e
|
@@ -86,15 +78,19 @@ module Breathing
|
|
86
78
|
private
|
87
79
|
|
88
80
|
def exists?(trigger_name)
|
89
|
-
|
90
|
-
@trigger_names.include?(trigger_name)
|
81
|
+
ActiveRecord::Base.connection.triggers.keys.include?(trigger_name)
|
91
82
|
end
|
92
83
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
84
|
+
def row_to_json(columns, state)
|
85
|
+
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
86
|
+
"row_to_json(#{state}.*)"
|
87
|
+
else
|
88
|
+
json_object_values = columns.each.with_object([]) do |column, array|
|
89
|
+
array << "'#{column.name}'"
|
90
|
+
array << "#{state}.#{column.name}"
|
91
|
+
end
|
92
|
+
"JSON_OBJECT(#{json_object_values.join(',')})"
|
93
|
+
end
|
98
94
|
end
|
99
95
|
end
|
100
96
|
end
|
data/spec/app.rb
CHANGED
@@ -2,7 +2,7 @@ require 'active_record'
|
|
2
2
|
require 'breathing'
|
3
3
|
|
4
4
|
ActiveRecord::Base.establish_connection(
|
5
|
-
YAML.load(ERB.new(File.read('spec/database.yml')).result)['
|
5
|
+
YAML.load(ERB.new(File.read('spec/database.yml')).result)["test_#{ENV['DB'] || 'mysql'}"]
|
6
6
|
)
|
7
7
|
|
8
8
|
ActiveRecord::Schema.define version: 0 do
|
@@ -3,7 +3,9 @@ require 'spec_helper'
|
|
3
3
|
describe Breathing::Excel do
|
4
4
|
describe '#create' do
|
5
5
|
before { Breathing::Installer.new.install }
|
6
|
-
after
|
6
|
+
after do
|
7
|
+
Breathing::Installer.new.uninstall if ActiveRecord::Base.connection.adapter_name == "Mysql2"
|
8
|
+
end
|
7
9
|
|
8
10
|
it do
|
9
11
|
user = User.create!(name: 'a', age: 20)
|
data/spec/breathing_spec.rb
CHANGED
@@ -5,36 +5,41 @@ describe Breathing do
|
|
5
5
|
expect(Breathing::VERSION).not_to be nil
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
8
|
+
describe 'change_logs' do
|
9
|
+
before { Breathing::Installer.new.install }
|
10
|
+
|
11
|
+
after do
|
12
|
+
Breathing::Installer.new.uninstall if ActiveRecord::Base.connection.adapter_name == "Mysql2"
|
13
|
+
end
|
14
|
+
|
15
|
+
it do
|
16
|
+
expect(Breathing::ChangeLog.count).to eq(0)
|
17
|
+
|
18
|
+
# INSERT
|
19
|
+
user = User.create!(name: 'a', age: 20)
|
20
|
+
expect(Breathing::ChangeLog.count).to eq(1)
|
21
|
+
|
22
|
+
log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
|
23
|
+
expect(log.before_data).to eq({})
|
24
|
+
expect(log.after_data['name']).to eq('a')
|
25
|
+
expect(log.after_data['age']).to eq(20)
|
26
|
+
|
27
|
+
# UPDATE
|
28
|
+
user.update!(age: 21)
|
29
|
+
expect(Breathing::ChangeLog.count).to eq(2)
|
30
|
+
|
31
|
+
log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
|
32
|
+
expect(log.before_data['age']).to eq(20)
|
33
|
+
expect(log.after_data['age']).to eq(21)
|
34
|
+
expect(log.before_data['name']).to eq(log.after_data['name'])
|
35
|
+
expect(log.changed_attribute_columns).to eq(%w[age updated_at])
|
36
|
+
|
37
|
+
# DELETE
|
38
|
+
user.destroy!
|
39
|
+
expect(Breathing::ChangeLog.count).to eq(3)
|
40
|
+
log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
|
41
|
+
expect(log.before_data['name']).to eq('a')
|
42
|
+
expect(log.after_data).to eq({})
|
43
|
+
end
|
39
44
|
end
|
40
45
|
end
|
data/spec/database.yml
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
-
|
1
|
+
test_mysql:
|
2
2
|
adapter: mysql2
|
3
3
|
encoding: utf8mb4
|
4
4
|
charset: utf8mb4
|
5
5
|
collation: utf8mb4_general_ci
|
6
|
-
pool: 5
|
7
6
|
username: <%= ENV.fetch("DB_USER") { 'root' } %>
|
8
7
|
password: <%= ENV.fetch("DB_PASS") { '' } %>
|
9
8
|
host: <%= ENV.fetch("DB_HOST") { '127.0.0.1' } %>
|
10
9
|
socket: /tmp/mysql.sock
|
11
|
-
database:
|
10
|
+
database: breathing_test
|
11
|
+
|
12
|
+
test_pg:
|
13
|
+
adapter: postgresql
|
14
|
+
encoding: unicode
|
15
|
+
username: <%= ENV.fetch("DB_USER") { 'root' } %>
|
16
|
+
password: <%= ENV.fetch("DB_PASS") { '' } %>
|
17
|
+
host: <%= ENV.fetch("DB_HOST") { '127.0.0.1' } %>
|
18
|
+
database: breathing_test
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: breathing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Akira Kusumoto
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-12-
|
11
|
+
date: 2020-12-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -39,19 +39,19 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 5.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: hairtrigger
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: '0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: mysql2
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,34 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubyXL
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.4.0
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.4.0
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: pry-byebug
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,7 +153,7 @@ homepage: https://github.com/bluerabbit/breathing
|
|
125
153
|
licenses:
|
126
154
|
- MIT
|
127
155
|
metadata: {}
|
128
|
-
post_install_message:
|
156
|
+
post_install_message:
|
129
157
|
rdoc_options: []
|
130
158
|
require_paths:
|
131
159
|
- lib
|
@@ -141,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
169
|
version: '0'
|
142
170
|
requirements: []
|
143
171
|
rubygems_version: 3.0.3
|
144
|
-
signing_key:
|
172
|
+
signing_key:
|
145
173
|
specification_version: 4
|
146
174
|
summary: Audit logging for database
|
147
175
|
test_files:
|