fury_dumper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ module Dumpers
5
+ class Model
6
+ attr_reader :source_model, :iteration, :warnings
7
+ attr_accessor :relation_items, :root_model
8
+
9
+ def initialize(source_model:, relation_items:, iteration: 0, root_model: nil)
10
+ raise ArgumentError unless source_model.is_a?(String)
11
+ raise ArgumentError unless relation_items.is_a?(RelationItems)
12
+ raise ArgumentError unless iteration.is_a?(Numeric)
13
+ raise ArgumentError unless root_model.is_a?(Model) || root_model.nil?
14
+
15
+ @source_model = source_model
16
+ @relation_items = relation_items
17
+ @iteration = iteration
18
+ @root_model = root_model
19
+ @warnings = []
20
+ end
21
+
22
+ def copy
23
+ self.class.new(source_model: source_model,
24
+ relation_items: relation_items.copy,
25
+ iteration: iteration,
26
+ root_model: root_model)
27
+ end
28
+
29
+ delegate :table_name, to: :active_record_model
30
+
31
+ def column_names
32
+ active_record_model.columns.map(&:name)
33
+ end
34
+
35
+ def ==(other)
36
+ raise ArgumentError unless other.is_a?(Model)
37
+
38
+ @source_model == other.source_model &&
39
+ @relation_items.eql?(other.relation_items) &&
40
+ sub_path?(other)
41
+ end
42
+
43
+ def sub_path?(other_model)
44
+ raise ArgumentError unless other_model.is_a?(Model)
45
+
46
+ min_length = [root_path.length, other_model.root_path.length].min - 1
47
+ root_path[0..min_length] == other_model.root_path[0..min_length]
48
+ end
49
+
50
+ def root_path
51
+ return [nil] if @root_model.nil?
52
+
53
+ @root_model.root_path + [@root_model.source_model]
54
+ end
55
+
56
+ def to_full_str
57
+ buffer = "MODEL #{@source_model} by #{root_model&.source_model.presence || '-'} WHERE "
58
+
59
+ buffer += @relation_items.items.map do |item|
60
+ if item.complex
61
+ item.key
62
+ elsif item.values_for_key.count > 10
63
+ "#{item.key} = #{item.values_for_key[0..10]} and #{item.values_for_key.count - 10} elements"
64
+ else
65
+ "#{item.key} = #{item.values_for_key}"
66
+ end
67
+ end.join(' AND ')
68
+
69
+ buffer
70
+ end
71
+
72
+ def active_record_model
73
+ @source_model.constantize
74
+ end
75
+
76
+ delegate :primary_key, to: :active_record_model
77
+
78
+ def to_short_str
79
+ "#{@source_model}.#{@relation_items.keys.join(' & ')}"
80
+ end
81
+
82
+ def fetch_complex_items
83
+ @relation_items.complex_items.map(&:key).join(' AND ')
84
+ end
85
+
86
+ def fetch_equality_items_hash
87
+ values = {}
88
+ @relation_items.equality_items.each do |item|
89
+ if active_record_model.column_names.include?(item.key)
90
+ values[item.key] = item.values_for_key
91
+ else
92
+ @warnings << "Shit relation: #{@source_model}.#{item.key} does not exist"
93
+ next
94
+ end
95
+ end
96
+
97
+ values
98
+ end
99
+
100
+ def all_non_scoped_models
101
+ active_record_model.reflect_on_all_associations.select { |rr| rr.scope.nil? }.map do |rr|
102
+ rr.klass.to_s
103
+ rescue NameError, LoadError => e
104
+ @warnings << e
105
+ next
106
+ end.compact.uniq
107
+ end
108
+
109
+ def to_active_record_relation
110
+ return @active_record_relation if @active_record_relation
111
+
112
+ complex_values = fetch_complex_items
113
+ equality_values = fetch_equality_items_hash
114
+ @active_record_relation = active_record_model.where(equality_values)
115
+ .where(complex_values)
116
+ .limit(FuryDumper::Config.limit)
117
+
118
+ unless FuryDumper::Config.fast?
119
+ order_value = { primary_key => :desc }
120
+ @active_record_relation = @active_record_relation.order(order_value)
121
+ end
122
+
123
+ @active_record_relation
124
+ end
125
+
126
+ def next_iteration
127
+ @iteration + 1
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ module Dumpers
5
+ class ModelQueue
6
+ def initialize
7
+ @queue = []
8
+ end
9
+
10
+ def add_element(model:, dump_state:)
11
+ raise ArgumentError, "Expected model as Dumpers::Model, got: #{model.class}" unless model.is_a?(Model)
12
+
13
+ unless dump_state.is_a?(DumpState)
14
+ raise ArgumentError,
15
+ "Expected dump_state as Dumpers::DumpState, got: #{dump_state.class}"
16
+ end
17
+
18
+ @queue << [model, dump_state]
19
+ end
20
+
21
+ def empty?
22
+ @queue.empty?
23
+ end
24
+
25
+ def fetch_element
26
+ @queue.delete_at(0)
27
+ end
28
+
29
+ def count
30
+ @queue.count
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ module Dumpers
5
+ RelationItem = Struct.new(:key, :values_for_key, :complex, :additional) do
6
+ def initialize(key:, values_for_key:, complex: false, additional: false)
7
+ super(key, values_for_key, complex, additional)
8
+ end
9
+
10
+ def eql?(other)
11
+ key == other.key
12
+ end
13
+
14
+ def copy
15
+ self.class.new(key: key, values_for_key: values_for_key.dup, complex: complex, additional: additional)
16
+ end
17
+ end
18
+
19
+ class RelationItems
20
+ attr_accessor :items
21
+
22
+ def initialize(items: [])
23
+ raise ArgumentError unless items.is_a?(Array)
24
+ raise ArgumentError unless items.all? { |item| item.is_a?(RelationItem) }
25
+
26
+ @items = items
27
+ end
28
+
29
+ def self.new_with_key_value(item_key: 'id', item_values: [])
30
+ new(items: [RelationItem.new(key: item_key, values_for_key: item_values.compact)])
31
+ end
32
+
33
+ def self.new_with_items(items: [])
34
+ new(items: items)
35
+ end
36
+
37
+ def eql?(other)
38
+ raise ArgumentError unless other.is_a?(RelationItems)
39
+
40
+ other.items.reject(&:additional).all? do |other_item|
41
+ items.reject(&:additional).any? { |item| item.eql?(other_item) }
42
+ end
43
+ end
44
+
45
+ def equality_items
46
+ items.reject(&:complex)
47
+ end
48
+
49
+ def complex_items
50
+ items.select(&:complex)
51
+ end
52
+
53
+ def keys
54
+ items.map(&:key).sort
55
+ end
56
+
57
+ def values(key)
58
+ items.select { |item| item.key == key }.values_for_key
59
+ end
60
+
61
+ def copy
62
+ self.class.new(items: copy_items)
63
+ end
64
+
65
+ def copy_items
66
+ items.map(&:copy)
67
+ end
68
+
69
+ def copy_with_new_values(key, new_values)
70
+ new_items = copy_items
71
+ new_items.each do |item|
72
+ item.values_for_key = new_values.compact.dup if item.key == key
73
+ end
74
+
75
+ self.class.new_with_items(items: new_items)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ class Encrypter
5
+ KEY = "\xBE\nXx\xE2\xDB\x85\xBD\xE1j}qz?}\xB0j6\xA95\xBAy80\x95\xE6\xC1\x9D\x9F\x89\xA2t"
6
+
7
+ def self.encrypt(msg)
8
+ crypt = ActiveSupport::MessageEncryptor.new(KEY)
9
+ crypt.encrypt_and_sign(msg)
10
+ end
11
+
12
+ def self.decrypt(encrypted_data)
13
+ crypt = ActiveSupport::MessageEncryptor.new(KEY)
14
+ crypt.decrypt_and_verify(encrypted_data)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace FuryDumper
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'fury_dumper/version'
5
+ require 'fury_dumper/config'
6
+ require 'fury_dumper/engine'
7
+ require 'fury_dumper/dumper'
8
+ require 'fury_dumper/api'
9
+ require 'fury_dumper/dumpers/relation_items'
10
+ require 'fury_dumper/dumpers/dump_state'
11
+ require 'fury_dumper/dumpers/model_queue'
12
+ require 'fury_dumper/dumpers/model'
13
+ require 'fury_dumper/encrypter'
14
+ require 'httpclient'
15
+ require 'highline/import'
16
+
17
+ module FuryDumper
18
+ class Error < StandardError
19
+ end
20
+
21
+ def self.configuration
22
+ @configuration ||= Config.config
23
+ end
24
+
25
+ # Start dumping
26
+ #
27
+ # @param password[String] - password for remote DB
28
+ # @param host[String] - host for remote DB
29
+ # @param port[String] - port for remote DB
30
+ # @param user[String] - username for remote DB
31
+ # @param model_name[String] - name of model for dump
32
+ # @param field_name[String] - field name for model
33
+ # @param field_values[Array|Range] - values of field_name
34
+ # @param database[String] - DB remote name
35
+ # @param debug_mode[Symbol] - debug mode (full - all msgs, short - part of msgs, none - nothing)
36
+ # @param ask[Boolean] - ask user for confirm different schema of target & remote DB
37
+ #
38
+ # @example FuryDumper.dump( password: '12345',
39
+ # host: 'localhost',
40
+ # port: '5432',
41
+ # user: 'username',
42
+ # model_name: 'User',
43
+ # field_name: 'admin_token',
44
+ # field_values: ['99999999-8888-4444-1212-111111111111'],
45
+ # database: 'staging',
46
+ # debug_mode: :short)
47
+ def self.dump(password:,
48
+ host:,
49
+ port:,
50
+ user:,
51
+ field_values:, database:, model_name: 'Lead',
52
+ field_name: 'id',
53
+ debug_mode: :none,
54
+ ask: true)
55
+
56
+ check_type(model_name, String, 'model name')
57
+ check_type(field_name, String, 'field name')
58
+ check_type(field_values, [Array, Range], 'field values')
59
+
60
+ states = []
61
+ field_values.to_a.in_groups_of(FuryDumper::Config.batch_size) do |batch|
62
+ relation_items = FuryDumper::Dumpers::RelationItems.new_with_key_value(item_key: field_name, item_values: batch)
63
+
64
+ sync = Dumper.new \
65
+ password: password,
66
+ host: host,
67
+ port: port,
68
+ user: user,
69
+ database: database,
70
+ model: FuryDumper::Dumpers::Model.new(source_model: model_name, relation_items: relation_items),
71
+ debug_mode: debug_mode
72
+
73
+ if ask && !sync.equal_schemas?
74
+ confirm = ask('Are you sure to continue? [Y/N] ') { |yn| yn.limit = 1, yn.validate = /[yn]/i }
75
+ ask = false
76
+ return unless confirm.downcase == 'y' # rubocop:disable Lint/NonLocalExitFromIterator
77
+ end
78
+
79
+ sync.sync_models
80
+ states << sync.dump_state
81
+ end
82
+
83
+ states.each_with_index do |state, index|
84
+ p "Batch ##{index}:"
85
+ state.print_statistic
86
+ end
87
+ true
88
+ end
89
+
90
+ def self.check_type(field, expected_type, field_name)
91
+ is_ok = if expected_type.is_a?(Array)
92
+ expected_type.any? do |type|
93
+ field.is_a?(type)
94
+ end
95
+ else
96
+ field.is_a?(expected_type)
97
+ end
98
+ types_str = expected_type.is_a?(Array) ? expected_type.join(' or ') : expected_type
99
+
100
+ raise ArgumentError, "Expected #{field_name} as #{types_str}, got: #{field.class}" unless is_ok
101
+ end
102
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FuryDumper
4
+ module Generators
5
+ class ConfigGenerator < Rails::Generators::Base
6
+ def self.gem_root
7
+ File.expand_path('../../..', __dir__)
8
+ end
9
+
10
+ def self.source_root
11
+ # Use the templates from the 2.3.x generator
12
+ File.join(gem_root, 'rails_generators', 'fury_dumper_config', 'templates')
13
+ end
14
+
15
+ def generate
16
+ template 'fury_dumper.rb', File.join('config', 'initializers', 'fury_dumper.rb')
17
+ template 'fury_dumper.yml', File.join('config', 'fury_dumper.yml')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FuryDumperConfigGenerator < Rails::Generators::Base
4
+ def manifest
5
+ record do |m|
6
+ m.template('fury_dumper.yml', 'config/fury_dumper.yml')
7
+ m.template('fury_dumper.rb', 'config/fury_dumper.rb')
8
+ end
9
+ end
10
+ end
@@ -0,0 +1 @@
1
+ FuryDumper::Config.load("#{Rails.root}/config/fury_dumper.yml")
@@ -0,0 +1,44 @@
1
+ exclude_relations: <class_name>.<relation_name>, <class_name>.<relation_name>
2
+ batch_size: 100
3
+ ratio_records_batches: 10
4
+ fast: true
5
+ mode: wide
6
+ relative_services:
7
+ <service_name>:
8
+ database: <database_name>
9
+ host: <host>
10
+ port: <port>
11
+ user: <user>
12
+ password: <password>
13
+ tables:
14
+ <self_table_name>:
15
+ <remote_table_name>:
16
+ self_field_name: <self_field_name>
17
+ ms_model_name: <ms_model_name>
18
+ ms_field_name: <ms_field_name>
19
+ <other_remote_table_name>:
20
+ self_field_name: <self_field_name>
21
+ ms_model_name: <ms_model_name>
22
+ ms_field_name: <ms_field_name>
23
+ <other_self_table_name>:
24
+ <remote_table_name>:
25
+ self_field_name: <self_field_name>
26
+ ms_model_name: <ms_model_name>
27
+ ms_field_name: <ms_field_name>
28
+ <other_remote_table_name>:
29
+ self_field_name: <self_field_name>
30
+ ms_model_name: <ms_model_name>
31
+ ms_field_name: <ms_field_name>
32
+ <other_service_name>:
33
+ database: <database_name>
34
+ host: <host>
35
+ port: <port>
36
+ user: <user>
37
+ password: <password>
38
+ tables:
39
+ <self_table_name>:
40
+ <remote_table_name>:
41
+ self_field_name: <self_field_name>
42
+ ms_model_name: <ms_model_name>
43
+ ms_field_name: <ms_field_name>
44
+
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fury_dumper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nastya Patutina
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: highline
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: httpclient
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pg
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 4.0.13
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '5.0'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 4.0.13
117
+ description: Dump from remote DB by lead_ids interval
118
+ email:
119
+ - npatutina@gmail.con
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".github/workflows/build.yaml"
125
+ - ".gitignore"
126
+ - ".rspec"
127
+ - ".rubocop.yml"
128
+ - ".ruby-version"
129
+ - ".travis.yml"
130
+ - Breadth-first.png
131
+ - CODE_OF_CONDUCT.md
132
+ - Depth-first.png
133
+ - Gemfile
134
+ - Gemfile.lock
135
+ - README.md
136
+ - README.ru.md
137
+ - Rakefile
138
+ - app/controllers/fury_dumper/dump_process_controller.rb
139
+ - config/routes.rb
140
+ - fury_dumper.gemspec
141
+ - lib/fury_dumper.rb
142
+ - lib/fury_dumper/api.rb
143
+ - lib/fury_dumper/config.rb
144
+ - lib/fury_dumper/dumper.rb
145
+ - lib/fury_dumper/dumpers/dump_state.rb
146
+ - lib/fury_dumper/dumpers/model.rb
147
+ - lib/fury_dumper/dumpers/model_queue.rb
148
+ - lib/fury_dumper/dumpers/relation_items.rb
149
+ - lib/fury_dumper/encrypter.rb
150
+ - lib/fury_dumper/engine.rb
151
+ - lib/fury_dumper/version.rb
152
+ - lib/generators/fury_dumper/config_generator.rb
153
+ - rails_generators/fury_dumper_config/fury_dumper_config_generator.rb
154
+ - rails_generators/fury_dumper_config/templates/fury_dumper.rb
155
+ - rails_generators/fury_dumper_config/templates/fury_dumper.yml
156
+ homepage: https://github.com/NastyaPatutina/fury_dumper
157
+ licenses: []
158
+ metadata:
159
+ homepage_uri: https://github.com/NastyaPatutina/fury_dumper
160
+ source_code_uri: https://github.com/NastyaPatutina/fury_dumper
161
+ rubygems_mfa_required: 'true'
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: 2.3.0
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ requirements: []
177
+ rubygems_version: 3.1.2
178
+ signing_key:
179
+ specification_version: 4
180
+ summary: Simple dump for main service and other microservices
181
+ test_files: []