breathing 0.0.1 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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