fury_dumper 0.1.0

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.
@@ -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: []