breathing 0.0.1 → 0.0.2

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: 9b0a34aef932a21f7369a766114261c484fcc68cdb32ec50e81c721da2f13d0a
4
+ data.tar.gz: eb6c8c5370a13e422674c9256032eeb650a4a58e018eed78f33c234a7a9cbc41
5
5
  SHA512:
6
- metadata.gz: 8ebf6e138f187609e34aee5f26f16b33a94efd98afb248a69c43286e4fe00e8496b176d8b3d8baa2a6dd59485b8ea7958dd1982c4ee329475f3bef04de1b3b86
7
- data.tar.gz: 798a892f5c78b97b4b7b7b61c6de09c74488953476901cd640da8e77e90d436d909e4873b9bc99f36f8d42db7ab53adf1a8fc7bad1be117e82f11af5432e36ad
6
+ metadata.gz: 91a00104ce4496d7250df3664624cd3cd5fb1b6af4d48f57445fbd7f0e798a4e4dd2003d0a2cfc3b1bc1e31d4729971abcfc0086701208a37b1a1975522527da
7
+ data.tar.gz: 4dc36063b0e355d5f245660136b2c439bacf16bf2da61fe899ee05b19a51f17c09e2036e1f6aa4505671c1f89cd4f4c637f0f58baf1adf0d1776bf6379594ccf
@@ -0,0 +1,53 @@
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_PASS: 'root'
11
+ DB_HOST: '127.0.0.1'
12
+ - image: circleci/mysql:8-ram
13
+ environment:
14
+ MYSQL_USER: root
15
+ MYSQL_ROOT_PASSWORD: root
16
+ MYSQL_DATABASE: breathing_test
17
+ command: [--default-authentication-plugin=mysql_native_password]
18
+
19
+ commands:
20
+ setup_bundle:
21
+ steps:
22
+ - restore_cache:
23
+ key: bundle-{{ checksum "breathing.gemspec" }}
24
+ - run:
25
+ name: install dependencies
26
+ command: |
27
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
28
+ - save_cache:
29
+ key: bundle-{{ checksum "breathing.gemspec" }}
30
+ paths:
31
+ - vendor/bundle
32
+
33
+ wait_for_db:
34
+ steps:
35
+ - run:
36
+ name: Wait for DB
37
+ command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m
38
+
39
+ jobs:
40
+ test:
41
+ executor: default
42
+ steps:
43
+ - checkout
44
+ - setup_bundle
45
+ - wait_for_db
46
+ - run: bundle exec rspec ./spec
47
+
48
+ workflows:
49
+ version: 2
50
+
51
+ test:
52
+ jobs:
53
+ - test
data/README.md CHANGED
@@ -28,9 +28,9 @@ Just run the following command.
28
28
 
29
29
  - Create table `change_logs`
30
30
  - Create triggers
31
- - breathing_insert_{table_name}
32
- - breathing_update_{table_name}
33
- - breathing_delete_{table_name}
31
+ - change_logs_insert_{table_name}
32
+ - change_logs_update_{table_name}
33
+ - change_logs_delete_{table_name}
34
34
 
35
35
  ### Uninstall
36
36
 
@@ -40,7 +40,17 @@ Cleanup command.
40
40
  % DATABASE_URL="mysql2://user:pass@host:port/database" breathing uninstall
41
41
  ```
42
42
 
43
- - Drop table `change_logs` and triggers
43
+ - Drop table `change_logs`
44
+ - Drop triggers
45
+ - change_logs_insert_{table_name}
46
+ - change_logs_update_{table_name}
47
+ - change_logs_delete_{table_name}
48
+
49
+ ### export
50
+
51
+ ```
52
+ % DATABASE_URL="mysql2://user:pass@host:port/database" breathing export
53
+ ```
44
54
 
45
55
  ## Copyright
46
56
 
@@ -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.2'
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,6 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_runtime_dependency 'thor'
22
22
 
23
23
  s.add_dependency 'activerecord', ['>= 5.0.0']
24
+ s.add_dependency 'rubyXL', ['>= 3.4.0']
24
25
  s.add_development_dependency 'mysql2'
25
26
  s.add_development_dependency 'pry-byebug'
26
27
  s.add_development_dependency 'rspec', '~> 3.9'
@@ -1,3 +1,35 @@
1
- class Breathing
1
+ require 'active_record'
2
+ require 'breathing/installer'
3
+ require 'breathing/trigger'
4
+ require 'breathing/change_log'
5
+ require 'breathing/excel'
6
+
7
+ module Breathing
2
8
  VERSION = Gem.loaded_specs['breathing'].version.to_s
9
+
10
+ class << self
11
+ def install
12
+ establish_connection
13
+ Breathing::Installer.new.install
14
+ end
15
+
16
+ def uninstall
17
+ establish_connection
18
+ Breathing::Installer.new.uninstall
19
+ end
20
+
21
+ def clear
22
+ establish_connection
23
+ Breathing::ChangeLog.delete_all
24
+ end
25
+
26
+ def export
27
+ establish_connection
28
+ Breathing::Excel.new.create
29
+ end
30
+
31
+ def establish_connection
32
+ ActiveRecord::Base.establish_connection(url: ENV['DATABASE_URL'])
33
+ end
34
+ end
3
35
  end
@@ -0,0 +1,16 @@
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
+ end
16
+ end
@@ -1,9 +1,29 @@
1
1
  require 'thor'
2
2
  require 'breathing'
3
3
 
4
- class Breathing
4
+ module Breathing
5
5
  class Cli < Thor
6
- default_command :version
6
+ default_command :install
7
+
8
+ desc 'install', 'Create table change_logs and create triggers'
9
+ def install
10
+ Breathing.install
11
+ end
12
+
13
+ desc 'uninstall', 'Drop table change_logs and drop triggers'
14
+ def uninstall
15
+ Breathing.uninstall
16
+ end
17
+
18
+ desc 'clear', 'Delete all records in change_logs table'
19
+ def clear
20
+ Breathing.clear
21
+ end
22
+
23
+ desc 'export', 'output xlsx'
24
+ def export
25
+ Breathing.export
26
+ end
7
27
 
8
28
  desc 'version', 'Show Version'
9
29
  def version
@@ -0,0 +1,74 @@
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
+
23
+ if first_row = rows.first
24
+ add_header_row(sheet, first_row)
25
+ end
26
+ add_body_rows(sheet, rows)
27
+ add_style(sheet)
28
+ end
29
+
30
+ @workbook.write(file_name)
31
+ end
32
+
33
+ private
34
+
35
+ def add_header_row(sheet, row)
36
+ sheet.add_cell(0, 0, 'change_logs.id').change_font_bold(true)
37
+ sheet.add_cell(0, 1, 'action').change_font_bold(true)
38
+ sheet.add_cell(0, 2, 'id').change_font_bold(true)
39
+ row.data_column_names.each.with_index(3) do |column_name, i|
40
+ sheet.add_cell(0, i, column_name).change_font_bold(true)
41
+ end
42
+ end
43
+
44
+ def add_body_rows(sheet, rows)
45
+ rows.each.with_index(1) do |row, i|
46
+ sheet.add_cell(i, 0, row.id)
47
+ sheet.add_cell(i, 1, row.action)
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])
52
+ if row.action == 'UPDATE' && column_name != 'updated_at' && row.changed_attribute_columns.include?(column_name)
53
+ cell_object.change_fill('ffff00') # color: yellow
54
+ elsif row.action == 'DELETE'
55
+ cell_object.change_fill('d9d9d9') # color: grey
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def add_style(sheet)
62
+ sheet.sheet_data.rows.each.with_index do |row, i|
63
+ row.cells.each do |cell|
64
+ %i[top bottom left right].each do |direction|
65
+ cell.change_border(direction, 'thin')
66
+ end
67
+
68
+ cell.change_border(:bottom, 'medium') if i.zero?
69
+ end
70
+ end
71
+ sheet.change_row_horizontal_alignment(0, 'center')
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,71 @@
1
+ require 'active_record'
2
+ require 'breathing'
3
+ require 'breathing/trigger'
4
+ require 'breathing/change_log'
5
+
6
+ module Breathing
7
+ class Installer
8
+ def install
9
+ create_log_table unless log_table_exists?
10
+
11
+ models.each do |model|
12
+ column_names = model.columns.map(&:name)
13
+ if column_names.include?('id') && column_names.include?('updated_at')
14
+ Breathing::Trigger.new(model, log_table_name).create
15
+ end
16
+ end
17
+ end
18
+
19
+ def uninstall
20
+ drop_log_table if log_table_exists?
21
+
22
+ models.each { |model| Breathing::Trigger.new(model, log_table_name).drop }
23
+ end
24
+
25
+ private
26
+
27
+ def log_table_name
28
+ Breathing::ChangeLog.table_name
29
+ end
30
+
31
+ def create_log_table(table_name: log_table_name)
32
+ ActiveRecord::Schema.define version: 0 do
33
+ create_table table_name, force: false do |t|
34
+ t.string :action, null: false
35
+ t.string :table_name, null: false
36
+ t.string :transaction_id, null: false
37
+ t.json :before_data, null: false
38
+ t.json :after_data, null: false
39
+ t.datetime :created_at, null: false, index: true
40
+ t.index %w[table_name transaction_id]
41
+ end
42
+ end
43
+ end
44
+
45
+ def drop_log_table
46
+ sql = "DROP TABLE #{log_table_name}"
47
+ puts sql
48
+ ActiveRecord::Base.connection.execute(sql)
49
+ end
50
+
51
+ def log_table_exists?
52
+ ActiveRecord::Base.connection.table_exists?(log_table_name)
53
+ end
54
+
55
+ def models
56
+ ignores = %w[schema_migrations ar_internal_metadata] << log_table_name
57
+
58
+ ActiveRecord::Base.connection.tables.each do |table_name|
59
+ next if ignores.include?(table_name) || Object.const_defined?(table_name.classify)
60
+
61
+ eval <<-EOS
62
+ class #{table_name.classify} < ActiveRecord::Base
63
+ self.table_name = :#{table_name}
64
+ end
65
+ EOS
66
+ end
67
+
68
+ ActiveRecord::Base.descendants.reject(&:abstract_class).reject { |m| ignores.include? m.table_name }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,100 @@
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
+ unless exists?(trigger_name)
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
31
+
32
+ trigger_name = "#{log_table_name}_update_#{model.table_name}"
33
+ unless exists?(trigger_name)
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
51
+
52
+ trigger_name = "#{log_table_name}_delete_#{model.table_name}"
53
+ unless exists?(trigger_name)
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
69
+ end
70
+
71
+ def drop
72
+ %w[insert update delete].each do |action|
73
+ trigger_name = "#{log_table_name}_#{action}_#{model.table_name}"
74
+ next unless exists?(trigger_name)
75
+
76
+ begin
77
+ sql = "DROP TRIGGER #{trigger_name}"
78
+ puts sql
79
+ ActiveRecord::Base.connection.execute(sql)
80
+ rescue StandardError => e
81
+ puts "#{e.message} trigger_name:#{trigger_name}"
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def exists?(trigger_name)
89
+ @trigger_names ||= ActiveRecord::Base.connection.select_rows('SHOW TRIGGERS').map { |row| row.first }
90
+ @trigger_names.include?(trigger_name)
91
+ end
92
+
93
+ def json_object_values(columns, state)
94
+ columns.each.with_object([]) do |column, array|
95
+ array << "'#{column.name}'"
96
+ array << "#{state}.#{column.name}"
97
+ end.join(',')
98
+ end
99
+ end
100
+ end
@@ -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,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Breathing::Excel do
4
+ describe '#create' do
5
+ before { Breathing::Installer.new.install }
6
+ after { Breathing::Installer.new.uninstall }
7
+
8
+ it do
9
+ user = User.create!(name: 'a', age: 20)
10
+ user.update!(age: 21)
11
+ user.destroy!
12
+ expect(Breathing::ChangeLog.count).to eq(3)
13
+
14
+ Tempfile.open(['tmp', '.xlsx']) do |file|
15
+ Breathing::Excel.new.create(file_name: file.path)
16
+ workbook = RubyXL::Parser.parse(file.path)
17
+ expect(workbook.sheets[0].name).to eq('users')
18
+ user_sheet = workbook.worksheets[0]
19
+ expect(user_sheet.sheet_data.size).to eq(Breathing::ChangeLog.where(table_name: :users).count + 1)
20
+ end
21
+ end
22
+
23
+ it 'multi sheets' do
24
+ User.create!(name: 'a', age: 20)
25
+ Department.create!(name: 'a')
26
+
27
+ Tempfile.open(['tmp', '.xlsx']) do |file|
28
+ Breathing::Excel.new.create(file_name: file.path)
29
+ workbook = RubyXL::Parser.parse(file.path)
30
+ expect(workbook.sheets.size).to eq(2)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,4 +4,37 @@ 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
+ before { Breathing::Installer.new.install }
9
+ after { Breathing::Installer.new.uninstall }
10
+
11
+ it do
12
+ expect(Breathing::ChangeLog.count).to eq(0)
13
+
14
+ # INSERT
15
+ user = User.create!(name: 'a', age: 20)
16
+ expect(Breathing::ChangeLog.count).to eq(1)
17
+
18
+ log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
19
+ expect(log.before_data).to eq({})
20
+ expect(log.after_data['name']).to eq('a')
21
+ expect(log.after_data['age']).to eq(20)
22
+
23
+ # UPDATE
24
+ user.update!(age: 21)
25
+ expect(Breathing::ChangeLog.count).to eq(2)
26
+
27
+ log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
28
+ expect(log.before_data['age']).to eq(20)
29
+ expect(log.after_data['age']).to eq(21)
30
+ expect(log.before_data['name']).to eq(log.after_data['name'])
31
+ expect(log.changed_attribute_columns).to eq(%w[age updated_at])
32
+
33
+ # DELETE
34
+ user.destroy!
35
+ expect(Breathing::ChangeLog.count).to eq(3)
36
+ log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
37
+ expect(log.before_data['name']).to eq('a')
38
+ expect(log.after_data).to eq({})
39
+ end
7
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akira Kusumoto
@@ -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: rubyXL
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.4.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.4.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: mysql2
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +94,7 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '3.9'
83
- description: audit logging for database
97
+ description: Audit logging for database
84
98
  email:
85
99
  - akirakusumo10@gmail.com
86
100
  executables:
@@ -88,6 +102,7 @@ executables:
88
102
  extensions: []
89
103
  extra_rdoc_files: []
90
104
  files:
105
+ - ".circleci/config.yml"
91
106
  - ".gitignore"
92
107
  - Gemfile
93
108
  - MIT-LICENSE
@@ -96,8 +111,13 @@ files:
96
111
  - breathing.gemspec
97
112
  - exe/breathing
98
113
  - lib/breathing.rb
114
+ - lib/breathing/change_log.rb
99
115
  - lib/breathing/cli.rb
116
+ - lib/breathing/excel.rb
117
+ - lib/breathing/installer.rb
118
+ - lib/breathing/trigger.rb
100
119
  - spec/app.rb
120
+ - spec/breathing/excel_spec.rb
101
121
  - spec/breathing_spec.rb
102
122
  - spec/database.yml
103
123
  - spec/spec_helper.rb
@@ -123,9 +143,10 @@ requirements: []
123
143
  rubygems_version: 3.0.3
124
144
  signing_key:
125
145
  specification_version: 4
126
- summary: audit logging for database
146
+ summary: Audit logging for database
127
147
  test_files:
128
148
  - spec/app.rb
149
+ - spec/breathing/excel_spec.rb
129
150
  - spec/breathing_spec.rb
130
151
  - spec/database.yml
131
152
  - spec/spec_helper.rb