breathing 0.0.5 → 0.0.10
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/README.md +24 -0
- data/breathing.gemspec +3 -2
- data/lib/breathing.rb +20 -0
- data/lib/breathing/change_log.rb +32 -0
- data/lib/breathing/cli.rb +26 -0
- data/lib/breathing/excel.rb +62 -57
- data/lib/breathing/installer.rb +14 -19
- data/lib/breathing/terminal_table.rb +30 -0
- data/lib/breathing/trigger.rb +49 -45
- data/spec/app.rb +3 -0
- data/spec/breathing/excel_spec.rb +11 -5
- data/spec/breathing/terminal_table_spec.rb +22 -0
- metadata +21 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9424309ec8f7ac77b44503b31bbd1a62fa54881cdaa7f75a507ba90d596d1f6a
|
4
|
+
data.tar.gz: d217036b86439348557fe9d0538be415260b970b43fa6fc0f29997c4772292d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfd1f75df70cb2380f9ed51000dcfacfdef64ffc185c9965cdc324979f676380c508ee7ed1e4d326bd134732c35aa430f8e10a211edfdf1e2bd30285a909ae84
|
7
|
+
data.tar.gz: 931f3a899695f04b044cf83f936db122291b28b85e7eb3924469745cd3b9cb44dd1c5366179c57f5e15720d78ac09dac4b1743062b21e34d6783b07bdb53d6cb
|
data/README.md
CHANGED
@@ -49,6 +49,30 @@ Cleanup command.
|
|
49
49
|
|
50
50
|
- Output file `breathing.xlsx`
|
51
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
|
+
|
52
76
|
## Compatibility
|
53
77
|
|
54
78
|
- Ruby 2.3.0+
|
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.10'
|
6
6
|
s.platform = Gem::Platform::RUBY
|
7
7
|
s.authors = ['Akira Kusumoto']
|
8
8
|
s.email = ['akirakusumo10@gmail.com']
|
@@ -20,10 +20,11 @@ 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', ['>=
|
23
|
+
s.add_dependency 'activerecord', ['>= 6.0.0']
|
24
24
|
s.add_dependency 'hairtrigger'
|
25
25
|
s.add_dependency 'mysql2'
|
26
26
|
s.add_dependency 'pg'
|
27
|
+
s.add_dependency 'terminal-table'
|
27
28
|
s.add_dependency 'rubyXL', ['>= 3.4.0']
|
28
29
|
s.add_development_dependency 'pry-byebug'
|
29
30
|
s.add_development_dependency 'rspec', '~> 3.9'
|
data/lib/breathing.rb
CHANGED
@@ -4,6 +4,7 @@ require 'breathing/installer'
|
|
4
4
|
require 'breathing/trigger'
|
5
5
|
require 'breathing/change_log'
|
6
6
|
require 'breathing/excel'
|
7
|
+
require 'breathing/terminal_table'
|
7
8
|
|
8
9
|
module Breathing
|
9
10
|
VERSION = Gem.loaded_specs['breathing'].version.to_s
|
@@ -28,5 +29,24 @@ module Breathing
|
|
28
29
|
ActiveRecord::Base.establish_connection
|
29
30
|
Breathing::Excel.new.create
|
30
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
|
31
51
|
end
|
32
52
|
end
|
data/lib/breathing/change_log.rb
CHANGED
@@ -12,5 +12,37 @@ 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
|
28
|
+
|
29
|
+
def diff
|
30
|
+
return nil if action != 'UPDATE'
|
31
|
+
|
32
|
+
changed_attribute_columns.map do |column_name|
|
33
|
+
"#{column_name}: #{before_data[column_name]} -> #{after_data[column_name]}"
|
34
|
+
end.join(" \n")
|
35
|
+
end
|
36
|
+
|
37
|
+
def attributes_for_excel
|
38
|
+
{
|
39
|
+
'change_logs.id' => id,
|
40
|
+
'created_at' => created_at.to_s(:db),
|
41
|
+
'table_name' => table_name,
|
42
|
+
'action' => action,
|
43
|
+
'id' => transaction_id,
|
44
|
+
'diff' => diff
|
45
|
+
}
|
46
|
+
end
|
15
47
|
end
|
16
48
|
end
|
data/lib/breathing/cli.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'thor'
|
2
3
|
require 'breathing'
|
3
4
|
|
@@ -6,26 +7,51 @@ module Breathing
|
|
6
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
@@ -11,98 +11,103 @@ module Breathing
|
|
11
11
|
def create(id: 1, file_name: 'breathing.xlsx')
|
12
12
|
sheet = @workbook[0]
|
13
13
|
table_names = Breathing::ChangeLog.where('id >= ?', id).group(:table_name).pluck(:table_name)
|
14
|
-
table_names.each do |table_name|
|
14
|
+
table_names.sort.each do |table_name|
|
15
15
|
if sheet.sheet_name == 'Sheet1'
|
16
16
|
sheet.sheet_name = table_name
|
17
17
|
else
|
18
18
|
sheet = @workbook.add_worksheet(table_name)
|
19
19
|
end
|
20
20
|
|
21
|
-
rows
|
22
|
-
column_widths = []
|
21
|
+
rows = Breathing::ChangeLog.where(table_name: table_name).where('id >= ?', id).order(:id).to_a
|
23
22
|
|
24
|
-
if
|
25
|
-
add_header_row(sheet, first_row, column_widths)
|
26
|
-
end
|
27
|
-
add_body_rows(sheet, rows, column_widths)
|
23
|
+
next if rows.size.zero?
|
28
24
|
|
29
|
-
|
30
|
-
|
31
|
-
end
|
25
|
+
add_header_row(sheet, rows.first)
|
26
|
+
add_body_rows(sheet, rows)
|
32
27
|
add_style(sheet)
|
33
28
|
end
|
34
29
|
|
30
|
+
if table_names.size.positive?
|
31
|
+
add_change_logs_sheet(id)
|
32
|
+
@workbook.worksheets.rotate!(-1)
|
33
|
+
end
|
35
34
|
@workbook.write(file_name)
|
36
35
|
end
|
37
36
|
|
38
37
|
private
|
39
38
|
|
40
|
-
def add_header_row(sheet, row
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
sheet.add_cell(0, 2, 'action').tap do |cell|
|
50
|
-
cell.change_font_bold(true)
|
51
|
-
cell.change_fill('ddedf3') # color: blue
|
52
|
-
end
|
53
|
-
sheet.add_cell(0, 3, 'id').tap do |cell|
|
54
|
-
cell.change_font_bold(true)
|
55
|
-
cell.change_fill('ddedf3') # color: blue
|
56
|
-
end
|
57
|
-
|
58
|
-
column_widths << 'change_logs.id'.size
|
59
|
-
column_widths << 'change_logs.created_at'.size
|
60
|
-
column_widths << 'action'.size
|
61
|
-
column_widths << 'id'.size
|
39
|
+
def add_header_row(sheet, row)
|
40
|
+
header_color = 'ddedf3' # blue
|
41
|
+
foreign_keys = ActiveRecord::Base.connection.foreign_keys(sheet.sheet_name)
|
42
|
+
row.data_attributes.keys.each.with_index do |header_column, column_index|
|
43
|
+
cell = sheet.add_cell(0, column_index, header_column)
|
44
|
+
cell.change_fill(header_color)
|
45
|
+
if column_index == 0
|
46
|
+
add_hyperlink(sheet: sheet, cell: cell, to_sheet: 'change_logs', to_cell: 'A1')
|
47
|
+
end
|
62
48
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
cell.change_fill('ddedf3') # color: blue
|
67
|
-
column_widths << column_name.size
|
49
|
+
if key = foreign_keys.detect { |k| k.options[:column] == header_column }
|
50
|
+
add_hyperlink(sheet: sheet, cell: cell, to_sheet: key.to_table, to_cell: 'A1')
|
51
|
+
end
|
68
52
|
end
|
69
53
|
end
|
70
54
|
|
71
|
-
def add_body_rows(sheet, rows
|
72
|
-
rows.each.with_index(1) do |row,
|
73
|
-
|
74
|
-
|
75
|
-
sheet.add_cell(i, 0, row.id)
|
76
|
-
sheet.add_cell(i, 1, row.created_at.to_s(:db))
|
77
|
-
sheet.add_cell(i, 2, row.action)
|
78
|
-
sheet.add_cell(i, 3, row.transaction_id)
|
79
|
-
|
80
|
-
data = row.action == 'DELETE' ? row.before_data : row.after_data
|
81
|
-
|
82
|
-
row.data_column_names.each.with_index(3) do |column_name, j|
|
83
|
-
value = data[column_name].to_s
|
84
|
-
column_widths[j] = value.size if column_widths[j] < value.size
|
85
|
-
cell_object = sheet.add_cell(i, j, value)
|
55
|
+
def add_body_rows(sheet, rows)
|
56
|
+
rows.each.with_index(1) do |row, row_number|
|
57
|
+
row.data_attributes.each.with_index do |(column_name, value), column_index|
|
58
|
+
cell = sheet.add_cell(row_number, column_index, value)
|
86
59
|
if row.action == 'UPDATE' && column_name != 'updated_at' && row.changed_attribute_columns.include?(column_name)
|
87
|
-
|
60
|
+
cell.change_fill('ffff00') # color: yellow
|
88
61
|
elsif row.action == 'DELETE'
|
89
|
-
|
62
|
+
cell.change_fill('d9d9d9') # color: grey
|
90
63
|
end
|
91
64
|
end
|
92
65
|
end
|
93
66
|
end
|
94
67
|
|
95
68
|
def add_style(sheet)
|
96
|
-
sheet.sheet_data.rows.each.with_index do |row,
|
97
|
-
row.cells.each do |cell|
|
69
|
+
sheet.sheet_data.rows.each.with_index do |row, row_index|
|
70
|
+
row.cells.each.with_index do |cell, column_index|
|
98
71
|
%i[top bottom left right].each do |direction|
|
99
72
|
cell.change_border(direction, 'thin')
|
100
73
|
end
|
74
|
+
cell.change_border(:bottom, 'double') if row_index.zero?
|
101
75
|
|
102
|
-
cell.
|
76
|
+
cell_width = cell.value.to_s.size + 2
|
77
|
+
sheet.change_column_width(column_index, cell_width) if cell_width > sheet.get_column_width(column_index)
|
103
78
|
end
|
104
79
|
end
|
80
|
+
|
105
81
|
sheet.change_row_horizontal_alignment(0, 'center')
|
106
82
|
end
|
83
|
+
|
84
|
+
def add_change_logs_sheet(id)
|
85
|
+
sheet = @workbook.add_worksheet(Breathing::ChangeLog.table_name)
|
86
|
+
|
87
|
+
change_logs = Breathing::ChangeLog.where('id >= ?', id).order(:id)
|
88
|
+
change_logs.first.attributes_for_excel.keys.each.with_index do |header_column, column_index|
|
89
|
+
cell = sheet.add_cell(0, column_index, header_column)
|
90
|
+
cell.change_fill('ddedf3') # blue
|
91
|
+
end
|
92
|
+
|
93
|
+
change_logs.each.with_index(1) do |change_log, row_number|
|
94
|
+
change_log.attributes_for_excel.each.with_index do |(column_name, value), column_index|
|
95
|
+
cell = sheet.add_cell(row_number, column_index, value)
|
96
|
+
|
97
|
+
if column_name == 'table_name'
|
98
|
+
add_hyperlink(sheet: sheet, cell: cell, to_sheet: value, to_cell: 'A1')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
add_style(sheet)
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_hyperlink(sheet:, cell:, to_sheet:, to_cell:)
|
107
|
+
sheet.hyperlinks ||= RubyXL::Hyperlinks.new
|
108
|
+
sheet.hyperlinks << RubyXL::Hyperlink.new(ref: RubyXL::Reference.ind2ref(cell.row, cell.column), location: "#{to_sheet}!#{to_cell}")
|
109
|
+
cell.change_font_underline(true)
|
110
|
+
cell.change_font_color('1F1FFF') # blue
|
111
|
+
end
|
107
112
|
end
|
108
113
|
end
|
data/lib/breathing/installer.rb
CHANGED
@@ -8,9 +8,9 @@ module Breathing
|
|
8
8
|
|
9
9
|
class Installer
|
10
10
|
def install
|
11
|
-
raise Breathing::UnsupportedError, "Version MySQL 5.6 is not supported." unless
|
11
|
+
raise Breathing::UnsupportedError, "Version MySQL 5.6 is not supported." unless database_supported_version?
|
12
12
|
|
13
|
-
create_log_table
|
13
|
+
create_log_table
|
14
14
|
|
15
15
|
models.each do |model|
|
16
16
|
column_names = model.columns.map(&:name)
|
@@ -21,14 +21,13 @@ module Breathing
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def uninstall
|
24
|
-
drop_log_table
|
25
|
-
|
24
|
+
drop_log_table
|
26
25
|
models.each { |model| Breathing::Trigger.new(model, log_table_name).drop }
|
27
26
|
end
|
28
27
|
|
29
28
|
private
|
30
29
|
|
31
|
-
def
|
30
|
+
def database_supported_version?
|
32
31
|
connection = ActiveRecord::Base.connection
|
33
32
|
connection.adapter_name == "PostgreSQL" || (connection.adapter_name == 'Mysql2' && connection.raw_connection.info[:version].to_f >= 5.7)
|
34
33
|
end
|
@@ -39,26 +38,22 @@ module Breathing
|
|
39
38
|
|
40
39
|
def create_log_table(table_name: log_table_name)
|
41
40
|
ActiveRecord::Schema.define version: 0 do
|
42
|
-
create_table table_name,
|
43
|
-
t.
|
44
|
-
t.string
|
45
|
-
t.string
|
46
|
-
t.
|
47
|
-
t.json
|
48
|
-
t.
|
41
|
+
create_table table_name, if_not_exists: true do |t|
|
42
|
+
t.datetime :created_at, null: false, index: true
|
43
|
+
t.string :table_name, null: false
|
44
|
+
t.string :action, null: false
|
45
|
+
t.string :transaction_id, null: false
|
46
|
+
t.json :before_data, null: false
|
47
|
+
t.json :after_data, null: false
|
48
|
+
|
49
49
|
t.index %w[table_name transaction_id]
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
54
|
def drop_log_table
|
55
|
-
|
56
|
-
|
57
|
-
ActiveRecord::Base.connection.execute(sql)
|
58
|
-
end
|
59
|
-
|
60
|
-
def log_table_exists?
|
61
|
-
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)
|
62
57
|
end
|
63
58
|
|
64
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,59 +11,25 @@ module Breathing
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def create
|
14
|
-
|
15
|
-
|
16
|
-
unless exists?(trigger_name)
|
17
|
-
puts "CREATE TRIGGER #{trigger_name}"
|
14
|
+
exists_trigger_names = ActiveRecord::Base.connection.triggers.keys
|
18
15
|
|
19
|
-
|
20
|
-
|
21
|
-
INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
|
22
|
-
VALUES ('INSERT', '#{model.table_name}', NEW.id,
|
23
|
-
'{}',
|
24
|
-
#{row_to_json(model.columns, 'NEW')},
|
25
|
-
CURRENT_TIMESTAMP);
|
26
|
-
SQL
|
27
|
-
end
|
28
|
-
end
|
16
|
+
trigger_name = "#{log_table_name}_insert_#{model.table_name}"
|
17
|
+
create_insert_trigger(trigger_name, model) if exists_trigger_names.exclude?(trigger_name)
|
29
18
|
|
30
19
|
trigger_name = "#{log_table_name}_update_#{model.table_name}"
|
31
|
-
|
32
|
-
puts "CREATE TRIGGER #{trigger_name}"
|
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)
|
37
|
-
VALUES ('UPDATE', '#{model.table_name}', NEW.id,
|
38
|
-
#{row_to_json(model.columns, 'OLD')},
|
39
|
-
#{row_to_json(model.columns, 'NEW')},
|
40
|
-
CURRENT_TIMESTAMP);
|
41
|
-
SQL
|
42
|
-
end
|
43
|
-
end
|
20
|
+
create_update_trigger(trigger_name, model) if exists_trigger_names.exclude?(trigger_name)
|
44
21
|
|
45
22
|
trigger_name = "#{log_table_name}_delete_#{model.table_name}"
|
46
|
-
|
47
|
-
puts "CREATE TRIGGER #{trigger_name}"
|
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
|
-
'{}',
|
54
|
-
CURRENT_TIMESTAMP);
|
55
|
-
SQL
|
56
|
-
end
|
57
|
-
end
|
23
|
+
create_delete_trigger(trigger_name, model) if exists_trigger_names.exclude?(trigger_name)
|
58
24
|
end
|
59
25
|
|
60
26
|
def drop
|
61
|
-
%w[insert update delete].
|
62
|
-
trigger_name = "#{log_table_name}_#{action}_#{model.table_name}"
|
27
|
+
trigger_names = %w[insert update delete].map { |action| "#{log_table_name}_#{action}_#{model.table_name}" }
|
63
28
|
|
29
|
+
trigger_names.each do |trigger_name|
|
64
30
|
begin
|
65
31
|
sql = "DROP TRIGGER IF EXISTS #{trigger_name}"
|
66
|
-
if
|
32
|
+
if postgresql?
|
67
33
|
sql << " ON #{model.table_name} CASCADE;"
|
68
34
|
sql << " DROP FUNCTION IF EXISTS #{trigger_name} CASCADE;"
|
69
35
|
end
|
@@ -77,12 +43,50 @@ module Breathing
|
|
77
43
|
|
78
44
|
private
|
79
45
|
|
80
|
-
def
|
81
|
-
ActiveRecord::Base.connection.
|
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
|
53
|
+
end
|
54
|
+
|
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
|
82
86
|
end
|
83
87
|
|
84
88
|
def row_to_json(columns, state)
|
85
|
-
if
|
89
|
+
if postgresql?
|
86
90
|
"row_to_json(#{state}.*)"
|
87
91
|
else
|
88
92
|
json_object_values = columns.each.with_object([]) do |column, array|
|
data/spec/app.rb
CHANGED
@@ -7,6 +7,7 @@ ActiveRecord::Base.establish_connection(
|
|
7
7
|
|
8
8
|
ActiveRecord::Schema.define version: 0 do
|
9
9
|
create_table :users, force: true do |t|
|
10
|
+
t.references :department, foreign_key: false
|
10
11
|
t.string :name, null: false
|
11
12
|
t.integer :age, null: false
|
12
13
|
t.timestamps null: false
|
@@ -16,6 +17,8 @@ ActiveRecord::Schema.define version: 0 do
|
|
16
17
|
t.string :name, null: false
|
17
18
|
t.timestamps null: false
|
18
19
|
end
|
20
|
+
|
21
|
+
add_foreign_key :users, :departments
|
19
22
|
end
|
20
23
|
|
21
24
|
class User < ActiveRecord::Base
|
@@ -16,20 +16,26 @@ describe Breathing::Excel do
|
|
16
16
|
Tempfile.open(['tmp', '.xlsx']) do |file|
|
17
17
|
Breathing::Excel.new.create(file_name: file.path)
|
18
18
|
workbook = RubyXL::Parser.parse(file.path)
|
19
|
-
expect(workbook.sheets
|
20
|
-
user_sheet = workbook.worksheets
|
19
|
+
expect(workbook.sheets.map(&:name)).to eq(%w[change_logs users])
|
20
|
+
user_sheet = workbook.worksheets.last
|
21
21
|
expect(user_sheet.sheet_data.size).to eq(Breathing::ChangeLog.where(table_name: :users).count + 1)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'multi sheets' do
|
26
|
-
|
27
|
-
|
26
|
+
department = Department.create!(name: 'a')
|
27
|
+
|
28
|
+
user = User.create!(name: 'a', age: 20, department_id: department.id)
|
29
|
+
user.update!(age: 21)
|
30
|
+
user.destroy!
|
28
31
|
|
29
32
|
Tempfile.open(['tmp', '.xlsx']) do |file|
|
30
33
|
Breathing::Excel.new.create(file_name: file.path)
|
31
34
|
workbook = RubyXL::Parser.parse(file.path)
|
32
|
-
expect(workbook.sheets.
|
35
|
+
expect(workbook.sheets.map(&:name)).to eq(%w[change_logs departments users])
|
36
|
+
change_logs_sheet = workbook.worksheets.first
|
37
|
+
|
38
|
+
expect(change_logs_sheet.sheet_data.size).to eq(Breathing::ChangeLog.count + 1)
|
33
39
|
end
|
34
40
|
end
|
35
41
|
end
|
@@ -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
|
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.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Akira Kusumoto
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -30,14 +30,14 @@ 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
42
|
name: hairtrigger
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
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'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rubyXL
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,9 +157,11 @@ files:
|
|
143
157
|
- lib/breathing/cli.rb
|
144
158
|
- lib/breathing/excel.rb
|
145
159
|
- lib/breathing/installer.rb
|
160
|
+
- lib/breathing/terminal_table.rb
|
146
161
|
- lib/breathing/trigger.rb
|
147
162
|
- spec/app.rb
|
148
163
|
- spec/breathing/excel_spec.rb
|
164
|
+
- spec/breathing/terminal_table_spec.rb
|
149
165
|
- spec/breathing_spec.rb
|
150
166
|
- spec/database.yml
|
151
167
|
- spec/spec_helper.rb
|
@@ -175,6 +191,7 @@ summary: Audit logging for database
|
|
175
191
|
test_files:
|
176
192
|
- spec/app.rb
|
177
193
|
- spec/breathing/excel_spec.rb
|
194
|
+
- spec/breathing/terminal_table_spec.rb
|
178
195
|
- spec/breathing_spec.rb
|
179
196
|
- spec/database.yml
|
180
197
|
- spec/spec_helper.rb
|