breathing 0.0.3 → 0.0.8
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 +36 -9
- data/breathing.gemspec +6 -3
- data/lib/breathing.rb +21 -0
- data/lib/breathing/change_log.rb +13 -0
- data/lib/breathing/cli.rb +27 -1
- data/lib/breathing/excel.rb +23 -20
- data/lib/breathing/installer.rb +21 -17
- data/lib/breathing/terminal_table.rb +30 -0
- data/lib/breathing/trigger.rb +62 -62
- data/spec/app.rb +1 -1
- data/spec/breathing/excel_spec.rb +3 -1
- data/spec/breathing/terminal_table_spec.rb +22 -0
- data/spec/breathing_spec.rb +36 -31
- data/spec/database.yml +10 -3
- data/spec/spec_helper.rb +2 -0
- metadata +55 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61e525b923beadee13acfa8b5dc2708b88a3d4e4add63ea910d4ee3fd67d42e4
|
4
|
+
data.tar.gz: 4ab65c2f8a026145dc121641922204bcd373f3ef1f379ced10b6575f1229e0a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c41a580a29c80ffeae3318a71153a6c6004a6a5f17bd02db91ecf3dbb8687dba7dd36318f5d3abbe4c86912a4103b4592aa9f6f4da446187deee072eaf84bccb
|
7
|
+
data.tar.gz: 5ffc9e9e41a1fa92ed44503b2df61dd6306fb2acff8ca390fc024eaa9468a1f65953d1de3c982e7d5a6e1b18f17a950f2c76075156a7af960ff8d84323d5c124
|
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
8
|
```
|
11
|
-
gem
|
12
|
-
```
|
13
|
-
|
14
|
-
Then bundle:
|
15
|
-
```
|
16
|
-
% bundle
|
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,44 @@ 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
|
+
### out
|
53
|
+
|
54
|
+
```
|
55
|
+
% DATABASE_URL="mysql2://user:pass@host:port/database" breathing out --table users --id 1
|
56
|
+
```
|
57
|
+
|
58
|
+
```
|
59
|
+
+----------------+------------------------+--------+----+-----+------+----------------------------+----------------------------+
|
60
|
+
| users |
|
61
|
+
+----------------+------------------------+--------+----+-----+------+----------------------------+----------------------------+
|
62
|
+
| change_logs.id | change_logs.created_at | action | id | age | name | created_at | updated_at |
|
63
|
+
+----------------+------------------------+--------+----+-----+------+----------------------------+----------------------------+
|
64
|
+
| 1 | 2020-12-18 22:43:32 | INSERT | 10 | 20 | a | 2020-12-18 13:43:32.316923 | 2020-12-18 13:43:32.316923 |
|
65
|
+
| 2 | 2020-12-18 22:43:32 | UPDATE | 10 | 21 | a | 2020-12-18 13:43:32.316923 | 2020-12-18 13:43:32.319706 |
|
66
|
+
| 3 | 2020-12-18 22:43:32 | DELETE | 10 | 21 | a | 2020-12-18 13:43:32.316923 | 2020-12-18 13:43:32.319706 |
|
67
|
+
+----------------+------------------------+--------+----+-----+------+----------------------------+----------------------------+
|
68
|
+
```
|
69
|
+
|
70
|
+
### tail
|
71
|
+
|
72
|
+
```
|
73
|
+
% DATABASE_URL="mysql2://user:pass@host:port/database" breathing tail --table users --id 1
|
74
|
+
```
|
75
|
+
|
76
|
+
## Compatibility
|
77
|
+
|
78
|
+
- Ruby 2.3.0+
|
79
|
+
- MySQL 5.7.0+
|
80
|
+
- PostgreSQL 8.0+
|
81
|
+
|
55
82
|
## Copyright
|
56
83
|
|
57
84
|
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.8'
|
6
6
|
s.platform = Gem::Platform::RUBY
|
7
7
|
s.authors = ['Akira Kusumoto']
|
8
8
|
s.email = ['akirakusumo10@gmail.com']
|
@@ -20,9 +20,12 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
21
|
s.add_runtime_dependency 'thor'
|
22
22
|
|
23
|
-
s.add_dependency 'activerecord', ['>=
|
24
|
-
s.add_dependency '
|
23
|
+
s.add_dependency 'activerecord', ['>= 6.0.0']
|
24
|
+
s.add_dependency 'hairtrigger'
|
25
25
|
s.add_dependency 'mysql2'
|
26
|
+
s.add_dependency 'pg'
|
27
|
+
s.add_dependency 'terminal-table'
|
28
|
+
s.add_dependency 'rubyXL', ['>= 3.4.0']
|
26
29
|
s.add_development_dependency 'pry-byebug'
|
27
30
|
s.add_development_dependency 'rspec', '~> 3.9'
|
28
31
|
end
|
data/lib/breathing.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'active_record'
|
2
|
+
require 'hairtrigger'
|
2
3
|
require 'breathing/installer'
|
3
4
|
require 'breathing/trigger'
|
4
5
|
require 'breathing/change_log'
|
5
6
|
require 'breathing/excel'
|
7
|
+
require 'breathing/terminal_table'
|
6
8
|
|
7
9
|
module Breathing
|
8
10
|
VERSION = Gem.loaded_specs['breathing'].version.to_s
|
@@ -27,5 +29,24 @@ module Breathing
|
|
27
29
|
ActiveRecord::Base.establish_connection
|
28
30
|
Breathing::Excel.new.create
|
29
31
|
end
|
32
|
+
|
33
|
+
def render_terminal_table(table_name:, id: 1)
|
34
|
+
ActiveRecord::Base.establish_connection
|
35
|
+
puts Breathing::TerminalTable.new(table_name).render(id: id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def tail_f(table_name:, id: 1)
|
39
|
+
ActiveRecord::Base.establish_connection
|
40
|
+
table = Breathing::TerminalTable.new(table_name)
|
41
|
+
|
42
|
+
loop do
|
43
|
+
text = table.render(id: id)
|
44
|
+
if text.present?
|
45
|
+
puts text
|
46
|
+
id = table.last_id + 1
|
47
|
+
end
|
48
|
+
sleep 5
|
49
|
+
end
|
50
|
+
end
|
30
51
|
end
|
31
52
|
end
|
data/lib/breathing/change_log.rb
CHANGED
@@ -12,5 +12,18 @@ module Breathing
|
|
12
12
|
names = before_data.keys.present? ? before_data.keys : after_data.keys
|
13
13
|
names.reject { |name| name == 'id' }
|
14
14
|
end
|
15
|
+
|
16
|
+
def data
|
17
|
+
action == 'DELETE' ? before_data : after_data
|
18
|
+
end
|
19
|
+
|
20
|
+
def data_attributes
|
21
|
+
data_column_names.each.with_object("change_logs.id" => id,
|
22
|
+
"change_logs.created_at" => created_at.to_s(:db),
|
23
|
+
"action" => action,
|
24
|
+
"id" => transaction_id) do |name, hash|
|
25
|
+
hash[name] = data[name]
|
26
|
+
end
|
27
|
+
end
|
15
28
|
end
|
16
29
|
end
|
data/lib/breathing/cli.rb
CHANGED
@@ -1,31 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'thor'
|
2
3
|
require 'breathing'
|
3
4
|
|
4
5
|
module Breathing
|
5
6
|
class Cli < Thor
|
6
|
-
default_command :
|
7
|
+
default_command :export
|
7
8
|
|
8
9
|
desc 'install', 'Create table change_logs and create triggers'
|
10
|
+
|
9
11
|
def install
|
10
12
|
Breathing.install
|
11
13
|
end
|
12
14
|
|
13
15
|
desc 'uninstall', 'Drop table change_logs and drop triggers'
|
16
|
+
|
14
17
|
def uninstall
|
15
18
|
Breathing.uninstall
|
16
19
|
end
|
17
20
|
|
18
21
|
desc 'clear', 'Delete all records in change_logs table'
|
22
|
+
|
19
23
|
def clear
|
20
24
|
Breathing.clear
|
21
25
|
end
|
22
26
|
|
23
27
|
desc 'export', 'output xlsx'
|
28
|
+
|
24
29
|
def export
|
25
30
|
Breathing.export
|
26
31
|
end
|
27
32
|
|
33
|
+
desc 'out', 'output stdout'
|
34
|
+
method_option :type, aliases: '-t', default: 'terminal_table', type: :string
|
35
|
+
method_option :table, type: :string, required: true
|
36
|
+
method_option :id, default: 1, type: :numeric
|
37
|
+
def out
|
38
|
+
if options[:table] == 'terminal_table'
|
39
|
+
Breathing.render_terminal_table(table_name: options[:table], id: options[:id].to_i)
|
40
|
+
else
|
41
|
+
# TODO
|
42
|
+
# Breathing.export(table_name: options[:table], id: options[:id].to_i)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc 'tail', 'tail terminal_table'
|
47
|
+
method_option :table, type: :string, required: true
|
48
|
+
method_option :id, default: 1, type: :numeric
|
49
|
+
def tail
|
50
|
+
Breathing.tail_f(table_name: options[:table], id: options[:id].to_i)
|
51
|
+
end
|
52
|
+
|
28
53
|
desc 'version', 'Show Version'
|
54
|
+
|
29
55
|
def version
|
30
56
|
say "Version: #{Breathing::VERSION}"
|
31
57
|
end
|
data/lib/breathing/excel.rb
CHANGED
@@ -18,12 +18,16 @@ module Breathing
|
|
18
18
|
sheet = @workbook.add_worksheet(table_name)
|
19
19
|
end
|
20
20
|
|
21
|
-
rows
|
21
|
+
rows = Breathing::ChangeLog.where(table_name: table_name).where('id >= ?', id).order(:id)
|
22
|
+
column_widths = []
|
22
23
|
|
23
24
|
if first_row = rows.first
|
24
|
-
add_header_row(sheet, first_row)
|
25
|
+
add_header_row(sheet, first_row, column_widths)
|
25
26
|
end
|
26
|
-
add_body_rows(sheet, rows)
|
27
|
+
add_body_rows(sheet, rows, column_widths)
|
28
|
+
|
29
|
+
column_widths.each.with_index { |size, i| sheet.change_column_width(i, size + 2) }
|
30
|
+
|
27
31
|
add_style(sheet)
|
28
32
|
end
|
29
33
|
|
@@ -32,28 +36,27 @@ module Breathing
|
|
32
36
|
|
33
37
|
private
|
34
38
|
|
35
|
-
def add_header_row(sheet, row)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
def add_header_row(sheet, row, column_widths)
|
40
|
+
header_color = 'ddedf3' # blue
|
41
|
+
row.data_attributes.keys.each.with_index do |header_column, column_index|
|
42
|
+
cell = sheet.add_cell(0, column_index, header_column)
|
43
|
+
cell.change_fill(header_color)
|
44
|
+
|
45
|
+
column_widths << header_column.size
|
41
46
|
end
|
42
47
|
end
|
43
48
|
|
44
|
-
def add_body_rows(sheet, rows)
|
45
|
-
rows.each.with_index(1) do |row,
|
46
|
-
|
47
|
-
|
48
|
-
sheet.add_cell(i, 2, row.transaction_id)
|
49
|
-
row.data_column_names.each.with_index(3) do |column_name, j|
|
50
|
-
data = row.action == 'DELETE' ? row.before_data : row.after_data
|
51
|
-
cell_object = sheet.add_cell(i, j, data[column_name])
|
49
|
+
def add_body_rows(sheet, rows, column_widths)
|
50
|
+
rows.each.with_index(1) do |row, row_number|
|
51
|
+
row.data_attributes.each.with_index do |(column_name, value), column_index|
|
52
|
+
cell = sheet.add_cell(row_number, column_index, value)
|
52
53
|
if row.action == 'UPDATE' && column_name != 'updated_at' && row.changed_attribute_columns.include?(column_name)
|
53
|
-
|
54
|
+
cell.change_fill('ffff00') # color: yellow
|
54
55
|
elsif row.action == 'DELETE'
|
55
|
-
|
56
|
+
cell.change_fill('d9d9d9') # color: grey
|
56
57
|
end
|
58
|
+
|
59
|
+
column_widths[column_index] = value.to_s.size if column_widths[column_index] < value.to_s.size
|
57
60
|
end
|
58
61
|
end
|
59
62
|
end
|
@@ -65,7 +68,7 @@ module Breathing
|
|
65
68
|
cell.change_border(direction, 'thin')
|
66
69
|
end
|
67
70
|
|
68
|
-
cell.change_border(:bottom, '
|
71
|
+
cell.change_border(:bottom, 'double') if i.zero?
|
69
72
|
end
|
70
73
|
end
|
71
74
|
sheet.change_row_horizontal_alignment(0, 'center')
|
data/lib/breathing/installer.rb
CHANGED
@@ -4,9 +4,13 @@ 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
|
9
|
-
|
11
|
+
raise Breathing::UnsupportedError, "Version MySQL 5.6 is not supported." unless database_supported_version?
|
12
|
+
|
13
|
+
create_log_table
|
10
14
|
|
11
15
|
models.each do |model|
|
12
16
|
column_names = model.columns.map(&:name)
|
@@ -17,39 +21,39 @@ module Breathing
|
|
17
21
|
end
|
18
22
|
|
19
23
|
def uninstall
|
20
|
-
drop_log_table
|
21
|
-
|
24
|
+
drop_log_table
|
22
25
|
models.each { |model| Breathing::Trigger.new(model, log_table_name).drop }
|
23
26
|
end
|
24
27
|
|
25
28
|
private
|
26
29
|
|
30
|
+
def database_supported_version?
|
31
|
+
connection = ActiveRecord::Base.connection
|
32
|
+
connection.adapter_name == "PostgreSQL" || (connection.adapter_name == 'Mysql2' && connection.raw_connection.info[:version].to_f >= 5.7)
|
33
|
+
end
|
34
|
+
|
27
35
|
def log_table_name
|
28
36
|
Breathing::ChangeLog.table_name
|
29
37
|
end
|
30
38
|
|
31
39
|
def create_log_table(table_name: log_table_name)
|
32
40
|
ActiveRecord::Schema.define version: 0 do
|
33
|
-
create_table table_name,
|
34
|
-
t.
|
35
|
-
t.string
|
36
|
-
t.string
|
37
|
-
t.
|
38
|
-
t.json
|
39
|
-
t.
|
41
|
+
create_table table_name, if_not_exists: true do |t|
|
42
|
+
t.datetime :created_at, null: false, index: true
|
43
|
+
t.string :action, null: false
|
44
|
+
t.string :table_name, null: false
|
45
|
+
t.string :transaction_id, null: false
|
46
|
+
t.json :before_data, null: false
|
47
|
+
t.json :after_data, null: false
|
48
|
+
|
40
49
|
t.index %w[table_name transaction_id]
|
41
50
|
end
|
42
51
|
end
|
43
52
|
end
|
44
53
|
|
45
54
|
def drop_log_table
|
46
|
-
|
47
|
-
|
48
|
-
ActiveRecord::Base.connection.execute(sql)
|
49
|
-
end
|
50
|
-
|
51
|
-
def log_table_exists?
|
52
|
-
ActiveRecord::Base.connection.table_exists?(log_table_name)
|
55
|
+
puts "DROP TABLE #{log_table_name}"
|
56
|
+
ActiveRecord::Base.connection.drop_table(log_table_name, if_exists: true)
|
53
57
|
end
|
54
58
|
|
55
59
|
def models
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'breathing'
|
2
|
+
require 'terminal-table'
|
3
|
+
|
4
|
+
module Breathing
|
5
|
+
class TerminalTable
|
6
|
+
attr_reader :last_id
|
7
|
+
|
8
|
+
def initialize(table_name)
|
9
|
+
@last_id = 1
|
10
|
+
@table_name = table_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def render(id: 1)
|
14
|
+
rows = Breathing::ChangeLog.where(table_name: @table_name).where("id >= ? ", id).order(:id)
|
15
|
+
|
16
|
+
return if rows.size.zero?
|
17
|
+
|
18
|
+
@table = Terminal::Table.new(title: rows.first.table_name,
|
19
|
+
headings: rows.first.data_attributes.keys,
|
20
|
+
rows: rows.map { |row| row.data_attributes.values })
|
21
|
+
|
22
|
+
@last_id = rows.last.id
|
23
|
+
@table.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def rows
|
27
|
+
@table.rows
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/breathing/trigger.rb
CHANGED
@@ -11,70 +11,28 @@ module Breathing
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def create
|
14
|
+
exists_trigger_names = ActiveRecord::Base.connection.triggers.keys
|
15
|
+
|
14
16
|
trigger_name = "#{log_table_name}_insert_#{model.table_name}"
|
15
|
-
|
16
|
-
puts "CREATE TRIGGER #{trigger_name}"
|
17
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
18
|
-
CREATE TRIGGER #{trigger_name}
|
19
|
-
AFTER INSERT
|
20
|
-
ON #{model.table_name}
|
21
|
-
FOR EACH ROW
|
22
|
-
BEGIN
|
23
|
-
INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
|
24
|
-
VALUES ('INSERT', '#{model.table_name}', NEW.id,
|
25
|
-
JSON_OBJECT(),
|
26
|
-
JSON_OBJECT(#{json_object_values(model.columns, 'NEW')}),
|
27
|
-
CURRENT_TIMESTAMP);
|
28
|
-
END;
|
29
|
-
SQL
|
30
|
-
end
|
17
|
+
create_insert_trigger(trigger_name, model) if exists_trigger_names.exclude?(trigger_name)
|
31
18
|
|
32
19
|
trigger_name = "#{log_table_name}_update_#{model.table_name}"
|
33
|
-
|
34
|
-
puts "CREATE TRIGGER #{trigger_name}"
|
35
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
36
|
-
CREATE TRIGGER #{trigger_name}
|
37
|
-
BEFORE UPDATE
|
38
|
-
ON #{model.table_name}
|
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`)
|
43
|
-
VALUES ('UPDATE', '#{model.table_name}', NEW.id,
|
44
|
-
JSON_OBJECT(#{json_object_values(model.columns, 'OLD')}),
|
45
|
-
JSON_OBJECT(#{json_object_values(model.columns, 'NEW')}),
|
46
|
-
CURRENT_TIMESTAMP);
|
47
|
-
end if;
|
48
|
-
END;
|
49
|
-
SQL
|
50
|
-
end
|
20
|
+
create_update_trigger(trigger_name, model) if exists_trigger_names.exclude?(trigger_name)
|
51
21
|
|
52
22
|
trigger_name = "#{log_table_name}_delete_#{model.table_name}"
|
53
|
-
|
54
|
-
puts "CREATE TRIGGER #{trigger_name}"
|
55
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
56
|
-
CREATE TRIGGER #{trigger_name}
|
57
|
-
AFTER DELETE
|
58
|
-
ON #{model.table_name}
|
59
|
-
FOR EACH ROW
|
60
|
-
BEGIN
|
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(),
|
65
|
-
CURRENT_TIMESTAMP);
|
66
|
-
END;
|
67
|
-
SQL
|
68
|
-
end
|
23
|
+
create_delete_trigger(trigger_name, model) if exists_trigger_names.exclude?(trigger_name)
|
69
24
|
end
|
70
25
|
|
71
26
|
def drop
|
72
|
-
%w[insert update delete].
|
73
|
-
trigger_name = "#{log_table_name}_#{action}_#{model.table_name}"
|
74
|
-
next unless exists?(trigger_name)
|
27
|
+
trigger_names = %w[insert update delete].map { |action| "#{log_table_name}_#{action}_#{model.table_name}" }
|
75
28
|
|
29
|
+
trigger_names.each do |trigger_name|
|
76
30
|
begin
|
77
|
-
sql = "DROP TRIGGER #{trigger_name}"
|
31
|
+
sql = "DROP TRIGGER IF EXISTS #{trigger_name}"
|
32
|
+
if postgresql?
|
33
|
+
sql << " ON #{model.table_name} CASCADE;"
|
34
|
+
sql << " DROP FUNCTION IF EXISTS #{trigger_name} CASCADE;"
|
35
|
+
end
|
78
36
|
puts sql
|
79
37
|
ActiveRecord::Base.connection.execute(sql)
|
80
38
|
rescue StandardError => e
|
@@ -85,16 +43,58 @@ module Breathing
|
|
85
43
|
|
86
44
|
private
|
87
45
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
46
|
+
def postgresql?
|
47
|
+
ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_trigger(name)
|
51
|
+
puts "CREATE TRIGGER #{name}"
|
52
|
+
ActiveRecord::Base.connection.create_trigger(name) # hairtrigger gem
|
91
53
|
end
|
92
54
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
55
|
+
def create_insert_trigger(trigger_name, model)
|
56
|
+
create_trigger(trigger_name).on(model.table_name).after(:insert) do
|
57
|
+
<<-SQL
|
58
|
+
INSERT INTO #{log_table_name} (created_at, action, table_name, transaction_id, before_data, after_data)
|
59
|
+
VALUES (CURRENT_TIMESTAMP, 'INSERT', '#{model.table_name}', NEW.id,
|
60
|
+
'{}',
|
61
|
+
#{row_to_json(model.columns, 'NEW')});
|
62
|
+
SQL
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_update_trigger(trigger_name, model)
|
67
|
+
create_trigger(trigger_name).on(model.table_name).before(:update).of(:updated_at) do
|
68
|
+
<<-SQL
|
69
|
+
INSERT INTO #{log_table_name} (created_at, action, table_name, transaction_id, before_data, after_data)
|
70
|
+
VALUES (CURRENT_TIMESTAMP, 'UPDATE', '#{model.table_name}', NEW.id,
|
71
|
+
#{row_to_json(model.columns, 'OLD')},
|
72
|
+
#{row_to_json(model.columns, 'NEW')});
|
73
|
+
SQL
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_delete_trigger(trigger_name, model)
|
78
|
+
create_trigger(trigger_name).on(model.table_name).after(:delete) do
|
79
|
+
<<-SQL
|
80
|
+
INSERT INTO #{log_table_name} (created_at, action, table_name, transaction_id, before_data, after_data)
|
81
|
+
VALUES (CURRENT_TIMESTAMP, 'DELETE', '#{model.table_name}', OLD.id,
|
82
|
+
#{row_to_json(model.columns, 'OLD')},
|
83
|
+
'{}');
|
84
|
+
SQL
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def row_to_json(columns, state)
|
89
|
+
if postgresql?
|
90
|
+
"row_to_json(#{state}.*)"
|
91
|
+
else
|
92
|
+
json_object_values = columns.each.with_object([]) do |column, array|
|
93
|
+
array << "'#{column.name}'"
|
94
|
+
array << "#{state}.#{column.name}"
|
95
|
+
end
|
96
|
+
"JSON_OBJECT(#{json_object_values.join(',')})"
|
97
|
+
end
|
98
98
|
end
|
99
99
|
end
|
100
100
|
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)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Breathing::TerminalTable do
|
4
|
+
describe '#render' do
|
5
|
+
before { Breathing::Installer.new.install }
|
6
|
+
after do
|
7
|
+
Breathing::Installer.new.uninstall if ActiveRecord::Base.connection.adapter_name == "Mysql2"
|
8
|
+
end
|
9
|
+
|
10
|
+
it do
|
11
|
+
user = User.create!(name: 'a', age: 20)
|
12
|
+
user.update!(age: 21)
|
13
|
+
user.destroy!
|
14
|
+
expect(Breathing::ChangeLog.count).to eq(3)
|
15
|
+
|
16
|
+
table = Breathing::TerminalTable.new(:users)
|
17
|
+
puts table.render(id: 1)
|
18
|
+
|
19
|
+
expect(table.rows.size).to eq(3)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
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.8
|
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-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -30,28 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 6.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 6.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,48 @@ 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: terminal-table
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubyXL
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.4.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 3.4.0
|
69
111
|
- !ruby/object:Gem::Dependency
|
70
112
|
name: pry-byebug
|
71
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,9 +157,11 @@ files:
|
|
115
157
|
- lib/breathing/cli.rb
|
116
158
|
- lib/breathing/excel.rb
|
117
159
|
- lib/breathing/installer.rb
|
160
|
+
- lib/breathing/terminal_table.rb
|
118
161
|
- lib/breathing/trigger.rb
|
119
162
|
- spec/app.rb
|
120
163
|
- spec/breathing/excel_spec.rb
|
164
|
+
- spec/breathing/terminal_table_spec.rb
|
121
165
|
- spec/breathing_spec.rb
|
122
166
|
- spec/database.yml
|
123
167
|
- spec/spec_helper.rb
|
@@ -125,7 +169,7 @@ homepage: https://github.com/bluerabbit/breathing
|
|
125
169
|
licenses:
|
126
170
|
- MIT
|
127
171
|
metadata: {}
|
128
|
-
post_install_message:
|
172
|
+
post_install_message:
|
129
173
|
rdoc_options: []
|
130
174
|
require_paths:
|
131
175
|
- lib
|
@@ -141,12 +185,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
185
|
version: '0'
|
142
186
|
requirements: []
|
143
187
|
rubygems_version: 3.0.3
|
144
|
-
signing_key:
|
188
|
+
signing_key:
|
145
189
|
specification_version: 4
|
146
190
|
summary: Audit logging for database
|
147
191
|
test_files:
|
148
192
|
- spec/app.rb
|
149
193
|
- spec/breathing/excel_spec.rb
|
194
|
+
- spec/breathing/terminal_table_spec.rb
|
150
195
|
- spec/breathing_spec.rb
|
151
196
|
- spec/database.yml
|
152
197
|
- spec/spec_helper.rb
|