breathing 0.0.1 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dd890230542703d28a6dcc8e7b6f1213cabf089027ebddc91280edda54cb322
4
- data.tar.gz: e29abf8900cc93a231c8ac1d12763d89bc8719ded8ddcb1fb68f5d7d7de6aa61
3
+ metadata.gz: a6108eb021949faaaaf3d08037dddd7dee71d4fe53592ed5991bc29c4ef9f724
4
+ data.tar.gz: 36219756e661c52c19879a0510e9452bacdd34bef21f7cb767dcf37e46d14ba0
5
5
  SHA512:
6
- metadata.gz: 8ebf6e138f187609e34aee5f26f16b33a94efd98afb248a69c43286e4fe00e8496b176d8b3d8baa2a6dd59485b8ea7958dd1982c4ee329475f3bef04de1b3b86
7
- data.tar.gz: 798a892f5c78b97b4b7b7b61c6de09c74488953476901cd640da8e77e90d436d909e4873b9bc99f36f8d42db7ab53adf1a8fc7bad1be117e82f11af5432e36ad
6
+ metadata.gz: 4e4d6f883ef9e715b52ca09cf3d69135c7f8c49eb7ada7da157ec6164ccb39321c3a9217547541cfe8040bae2c2480328ad9568694f227ba10ae3e72d1246c58
7
+ data.tar.gz: 05b8557f1c107daf79c00e086554daddb51e8987327dfb8dafd5f030f85e25cca6b8df53e1192fd2902fd3e62820b2cacc8019227c5f07c27ac3ab02f835a628
@@ -0,0 +1,60 @@
1
+ version: 2.1
2
+
3
+ executors:
4
+ default:
5
+ working_directory: ~/app
6
+ docker:
7
+ - image: circleci/ruby:2.6
8
+ environment:
9
+ DB_USER: root
10
+ DB_HOST: '127.0.0.1'
11
+ - image: circleci/mysql:8-ram
12
+ environment:
13
+ MYSQL_USER: root
14
+ MYSQL_ROOT_PASSWORD: root
15
+ MYSQL_DATABASE: breathing_test
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
21
+
22
+ commands:
23
+ setup_bundle:
24
+ steps:
25
+ - restore_cache:
26
+ key: bundle-{{ checksum "breathing.gemspec" }}
27
+ - run:
28
+ name: install dependencies
29
+ command: |
30
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
31
+ - save_cache:
32
+ key: bundle-{{ checksum "breathing.gemspec" }}
33
+ paths:
34
+ - vendor/bundle
35
+
36
+ wait_for_db:
37
+ steps:
38
+ - run:
39
+ name: Wait for DB
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
44
+
45
+ jobs:
46
+ test:
47
+ executor: default
48
+ steps:
49
+ - checkout
50
+ - setup_bundle
51
+ - wait_for_db
52
+ - run: DB=mysql DB_PASS=root bundle exec rspec ./spec
53
+ - run: DB=pg bundle exec rspec ./spec
54
+
55
+ workflows:
56
+ version: 2
57
+
58
+ test:
59
+ jobs:
60
+ - test
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
- % bundle
9
+ gem install breathing
17
10
  ```
18
11
 
19
12
  ## Usage
@@ -24,13 +17,15 @@ 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`
30
25
  - Create triggers
31
- - breathing_insert_{table_name}
32
- - breathing_update_{table_name}
33
- - breathing_delete_{table_name}
26
+ - change_logs_insert_{table_name}
27
+ - change_logs_update_{table_name}
28
+ - change_logs_delete_{table_name}
34
29
 
35
30
  ### Uninstall
36
31
 
@@ -40,7 +35,49 @@ Cleanup command.
40
35
  % DATABASE_URL="mysql2://user:pass@host:port/database" breathing uninstall
41
36
  ```
42
37
 
43
- - Drop table `change_logs` and triggers
38
+ - Drop table `change_logs`
39
+ - Drop triggers
40
+ - change_logs_insert_{table_name}
41
+ - change_logs_update_{table_name}
42
+ - change_logs_delete_{table_name}
43
+
44
+ ### Export
45
+
46
+ ```
47
+ % DATABASE_URL="mysql2://user:pass@host:port/database" breathing export
48
+ ```
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+
44
81
 
45
82
  ## Copyright
46
83
 
@@ -2,13 +2,13 @@ $:.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.1'
5
+ s.version = '0.0.6'
6
6
  s.platform = Gem::Platform::RUBY
7
7
  s.authors = ['Akira Kusumoto']
8
8
  s.email = ['akirakusumo10@gmail.com']
9
9
  s.homepage = 'https://github.com/bluerabbit/breathing'
10
- s.summary = 'audit logging for database'
11
- s.description = 'audit logging for database'
10
+ s.summary = 'Audit logging for database'
11
+ s.description = 'Audit logging for database'
12
12
 
13
13
  s.files = `git ls-files`.split("\n")
14
14
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -21,7 +21,11 @@ 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_development_dependency 'mysql2'
24
+ s.add_dependency 'hairtrigger'
25
+ s.add_dependency 'mysql2'
26
+ s.add_dependency 'pg'
27
+ s.add_dependency 'terminal-table'
28
+ s.add_dependency 'rubyXL', ['>= 3.4.0']
25
29
  s.add_development_dependency 'pry-byebug'
26
30
  s.add_development_dependency 'rspec', '~> 3.9'
27
31
  end
@@ -1,3 +1,52 @@
1
- class Breathing
1
+ require 'active_record'
2
+ require 'hairtrigger'
3
+ require 'breathing/installer'
4
+ require 'breathing/trigger'
5
+ require 'breathing/change_log'
6
+ require 'breathing/excel'
7
+ require 'breathing/terminal_table'
8
+
9
+ module Breathing
2
10
  VERSION = Gem.loaded_specs['breathing'].version.to_s
11
+
12
+ class << self
13
+ def install
14
+ ActiveRecord::Base.establish_connection
15
+ Breathing::Installer.new.install
16
+ end
17
+
18
+ def uninstall
19
+ ActiveRecord::Base.establish_connection
20
+ Breathing::Installer.new.uninstall
21
+ end
22
+
23
+ def clear
24
+ ActiveRecord::Base.establish_connection
25
+ Breathing::ChangeLog.delete_all
26
+ end
27
+
28
+ def export
29
+ ActiveRecord::Base.establish_connection
30
+ Breathing::Excel.new.create
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
51
+ end
3
52
  end
@@ -0,0 +1,29 @@
1
+ require 'active_record'
2
+
3
+ module Breathing
4
+ class ChangeLog < ActiveRecord::Base
5
+ def changed_attribute_columns
6
+ before_data.each.with_object([]) do |(column, value), columns|
7
+ columns << column if after_data[column] != value
8
+ end
9
+ end
10
+
11
+ def data_column_names
12
+ names = before_data.keys.present? ? before_data.keys : after_data.keys
13
+ names.reject { |name| name == 'id' }
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
+ end
29
+ end
@@ -1,11 +1,57 @@
1
+ # frozen_string_literal: true
1
2
  require 'thor'
2
3
  require 'breathing'
3
4
 
4
- class Breathing
5
+ module Breathing
5
6
  class Cli < Thor
6
- default_command :version
7
+ default_command :export
8
+
9
+ desc 'install', 'Create table change_logs and create triggers'
10
+
11
+ def install
12
+ Breathing.install
13
+ end
14
+
15
+ desc 'uninstall', 'Drop table change_logs and drop triggers'
16
+
17
+ def uninstall
18
+ Breathing.uninstall
19
+ end
20
+
21
+ desc 'clear', 'Delete all records in change_logs table'
22
+
23
+ def clear
24
+ Breathing.clear
25
+ end
26
+
27
+ desc 'export', 'output xlsx'
28
+
29
+ def export
30
+ Breathing.export
31
+ end
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
7
52
 
8
53
  desc 'version', 'Show Version'
54
+
9
55
  def version
10
56
  say "Version: #{Breathing::VERSION}"
11
57
  end
@@ -0,0 +1,108 @@
1
+ require 'rubyXL'
2
+ require 'rubyXL/convenience_methods'
3
+ require 'breathing'
4
+
5
+ module Breathing
6
+ class Excel
7
+ def initialize
8
+ @workbook = RubyXL::Workbook.new
9
+ end
10
+
11
+ def create(id: 1, file_name: 'breathing.xlsx')
12
+ sheet = @workbook[0]
13
+ table_names = Breathing::ChangeLog.where('id >= ?', id).group(:table_name).pluck(:table_name)
14
+ table_names.each do |table_name|
15
+ if sheet.sheet_name == 'Sheet1'
16
+ sheet.sheet_name = table_name
17
+ else
18
+ sheet = @workbook.add_worksheet(table_name)
19
+ end
20
+
21
+ rows = Breathing::ChangeLog.where(table_name: table_name).where('id >= ?', id).order(:id)
22
+ column_widths = []
23
+
24
+ if first_row = rows.first
25
+ add_header_row(sheet, first_row, column_widths)
26
+ end
27
+ add_body_rows(sheet, rows, column_widths)
28
+
29
+ column_widths.each.with_index(0) do |size, i|
30
+ sheet.change_column_width(i, size + 2)
31
+ end
32
+ add_style(sheet)
33
+ end
34
+
35
+ @workbook.write(file_name)
36
+ end
37
+
38
+ private
39
+
40
+ def add_header_row(sheet, row, column_widths)
41
+ sheet.add_cell(0, 0, 'change_logs.id').tap do |cell|
42
+ cell.change_font_bold(true)
43
+ cell.change_fill('ddedf3') # color: blue
44
+ end
45
+ sheet.add_cell(0, 1, 'change_logs.created_at').tap do |cell|
46
+ cell.change_font_bold(true)
47
+ cell.change_fill('ddedf3') # color: blue
48
+ end
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
62
+
63
+ row.data_column_names.each.with_index(3) do |column_name, i|
64
+ cell = sheet.add_cell(0, i, column_name)
65
+ cell.change_font_bold(true)
66
+ cell.change_fill('ddedf3') # color: blue
67
+ column_widths << column_name.size
68
+ end
69
+ end
70
+
71
+ def add_body_rows(sheet, rows, column_widths)
72
+ rows.each.with_index(1) do |row, i|
73
+ column_widths[0] = row.id.to_s.size if column_widths[0] < row.id.to_s.size
74
+ column_widths[2] = row.transaction_id.to_s.size if column_widths[2] < row.transaction_id.to_s.size
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)
86
+ if row.action == 'UPDATE' && column_name != 'updated_at' && row.changed_attribute_columns.include?(column_name)
87
+ cell_object.change_fill('ffff00') # color: yellow
88
+ elsif row.action == 'DELETE'
89
+ cell_object.change_fill('d9d9d9') # color: grey
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def add_style(sheet)
96
+ sheet.sheet_data.rows.each.with_index do |row, i|
97
+ row.cells.each do |cell|
98
+ %i[top bottom left right].each do |direction|
99
+ cell.change_border(direction, 'thin')
100
+ end
101
+
102
+ cell.change_border(:bottom, 'double') if i.zero?
103
+ end
104
+ end
105
+ sheet.change_row_horizontal_alignment(0, 'center')
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,80 @@
1
+ require 'active_record'
2
+ require 'breathing'
3
+ require 'breathing/trigger'
4
+ require 'breathing/change_log'
5
+
6
+ module Breathing
7
+ class UnsupportedError < StandardError; end
8
+
9
+ class Installer
10
+ def install
11
+ raise Breathing::UnsupportedError, "Version MySQL 5.6 is not supported." unless database_version_valid?
12
+
13
+ create_log_table unless log_table_exists?
14
+
15
+ models.each do |model|
16
+ column_names = model.columns.map(&:name)
17
+ if column_names.include?('id') && column_names.include?('updated_at')
18
+ Breathing::Trigger.new(model, log_table_name).create
19
+ end
20
+ end
21
+ end
22
+
23
+ def uninstall
24
+ drop_log_table if log_table_exists?
25
+
26
+ models.each { |model| Breathing::Trigger.new(model, log_table_name).drop }
27
+ end
28
+
29
+ private
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
+
36
+ def log_table_name
37
+ Breathing::ChangeLog.table_name
38
+ end
39
+
40
+ def create_log_table(table_name: log_table_name)
41
+ ActiveRecord::Schema.define version: 0 do
42
+ create_table table_name, force: false do |t|
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
+ t.datetime :created_at, null: false, index: true
49
+ t.index %w[table_name transaction_id]
50
+ end
51
+ end
52
+ end
53
+
54
+ def drop_log_table
55
+ sql = "DROP TABLE #{log_table_name}"
56
+ puts sql
57
+ ActiveRecord::Base.connection.execute(sql)
58
+ end
59
+
60
+ def log_table_exists?
61
+ ActiveRecord::Base.connection.table_exists?(log_table_name)
62
+ end
63
+
64
+ def models
65
+ ignores = %w[schema_migrations ar_internal_metadata] << log_table_name
66
+
67
+ ActiveRecord::Base.connection.tables.each do |table_name|
68
+ next if ignores.include?(table_name) || Object.const_defined?(table_name.classify)
69
+
70
+ eval <<-EOS
71
+ class #{table_name.classify} < ActiveRecord::Base
72
+ self.table_name = :#{table_name}
73
+ end
74
+ EOS
75
+ end
76
+
77
+ ActiveRecord::Base.descendants.reject(&:abstract_class).reject { |m| ignores.include? m.table_name }
78
+ end
79
+ end
80
+ end
@@ -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
@@ -0,0 +1,96 @@
1
+ require 'active_record'
2
+ require 'breathing/change_log'
3
+
4
+ module Breathing
5
+ class Trigger
6
+ attr_reader :model, :log_table_name
7
+
8
+ def initialize(model, log_table_name)
9
+ @model = model
10
+ @log_table_name = log_table_name
11
+ end
12
+
13
+ def create
14
+ trigger_name = "#{log_table_name}_insert_#{model.table_name}"
15
+
16
+ unless exists?(trigger_name)
17
+ puts "CREATE TRIGGER #{trigger_name}"
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)
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
29
+
30
+ trigger_name = "#{log_table_name}_update_#{model.table_name}"
31
+ unless exists?(trigger_name)
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
44
+
45
+ trigger_name = "#{log_table_name}_delete_#{model.table_name}"
46
+ unless exists?(trigger_name)
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
58
+ end
59
+
60
+ def drop
61
+ %w[insert update delete].each do |action|
62
+ trigger_name = "#{log_table_name}_#{action}_#{model.table_name}"
63
+
64
+ begin
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
70
+ puts sql
71
+ ActiveRecord::Base.connection.execute(sql)
72
+ rescue StandardError => e
73
+ puts "#{e.message} trigger_name:#{trigger_name}"
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def exists?(trigger_name)
81
+ ActiveRecord::Base.connection.triggers.keys.include?(trigger_name)
82
+ end
83
+
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
94
+ end
95
+ end
96
+ end
@@ -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)['test']
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
@@ -11,7 +11,15 @@ ActiveRecord::Schema.define version: 0 do
11
11
  t.integer :age, null: false
12
12
  t.timestamps null: false
13
13
  end
14
+
15
+ create_table :departments, force: true do |t|
16
+ t.string :name, null: false
17
+ t.timestamps null: false
18
+ end
14
19
  end
15
20
 
16
21
  class User < ActiveRecord::Base
17
22
  end
23
+
24
+ class Department < ActiveRecord::Base
25
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Breathing::Excel do
4
+ describe '#create' 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
+ Tempfile.open(['tmp', '.xlsx']) do |file|
17
+ Breathing::Excel.new.create(file_name: file.path)
18
+ workbook = RubyXL::Parser.parse(file.path)
19
+ expect(workbook.sheets[0].name).to eq('users')
20
+ user_sheet = workbook.worksheets[0]
21
+ expect(user_sheet.sheet_data.size).to eq(Breathing::ChangeLog.where(table_name: :users).count + 1)
22
+ end
23
+ end
24
+
25
+ it 'multi sheets' do
26
+ User.create!(name: 'a', age: 20)
27
+ Department.create!(name: 'a')
28
+
29
+ Tempfile.open(['tmp', '.xlsx']) do |file|
30
+ Breathing::Excel.new.create(file_name: file.path)
31
+ workbook = RubyXL::Parser.parse(file.path)
32
+ expect(workbook.sheets.size).to eq(2)
33
+ end
34
+ end
35
+ end
36
+ 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
@@ -4,4 +4,42 @@ describe Breathing do
4
4
  it 'has a version number' do
5
5
  expect(Breathing::VERSION).not_to be nil
6
6
  end
7
+
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
44
+ end
7
45
  end
@@ -1,11 +1,18 @@
1
- test:
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: breathing_test
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
@@ -4,6 +4,8 @@ require 'active_record'
4
4
  require 'breathing'
5
5
  require_relative 'app'
6
6
 
7
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
8
+
7
9
  RSpec.configure do |config|
8
10
  config.around do |example|
9
11
  ActiveRecord::Base.transaction do
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.1
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akira Kusumoto
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-10 00:00:00.000000000 Z
11
+ date: 2020-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 5.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: hairtrigger
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: mysql2
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -45,13 +59,55 @@ dependencies:
45
59
  - - ">="
46
60
  - !ruby/object:Gem::Version
47
61
  version: '0'
48
- type: :development
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
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
49
91
  prerelease: false
50
92
  version_requirements: !ruby/object:Gem::Requirement
51
93
  requirements:
52
94
  - - ">="
53
95
  - !ruby/object:Gem::Version
54
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
55
111
  - !ruby/object:Gem::Dependency
56
112
  name: pry-byebug
57
113
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +136,7 @@ dependencies:
80
136
  - - "~>"
81
137
  - !ruby/object:Gem::Version
82
138
  version: '3.9'
83
- description: audit logging for database
139
+ description: Audit logging for database
84
140
  email:
85
141
  - akirakusumo10@gmail.com
86
142
  executables:
@@ -88,6 +144,7 @@ executables:
88
144
  extensions: []
89
145
  extra_rdoc_files: []
90
146
  files:
147
+ - ".circleci/config.yml"
91
148
  - ".gitignore"
92
149
  - Gemfile
93
150
  - MIT-LICENSE
@@ -96,8 +153,15 @@ files:
96
153
  - breathing.gemspec
97
154
  - exe/breathing
98
155
  - lib/breathing.rb
156
+ - lib/breathing/change_log.rb
99
157
  - lib/breathing/cli.rb
158
+ - lib/breathing/excel.rb
159
+ - lib/breathing/installer.rb
160
+ - lib/breathing/terminal_table.rb
161
+ - lib/breathing/trigger.rb
100
162
  - spec/app.rb
163
+ - spec/breathing/excel_spec.rb
164
+ - spec/breathing/terminal_table_spec.rb
101
165
  - spec/breathing_spec.rb
102
166
  - spec/database.yml
103
167
  - spec/spec_helper.rb
@@ -123,9 +187,11 @@ requirements: []
123
187
  rubygems_version: 3.0.3
124
188
  signing_key:
125
189
  specification_version: 4
126
- summary: audit logging for database
190
+ summary: Audit logging for database
127
191
  test_files:
128
192
  - spec/app.rb
193
+ - spec/breathing/excel_spec.rb
194
+ - spec/breathing/terminal_table_spec.rb
129
195
  - spec/breathing_spec.rb
130
196
  - spec/database.yml
131
197
  - spec/spec_helper.rb