db_blaster 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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +46 -0
  4. data/Rakefile +20 -0
  5. data/app/assets/config/db_blaster_manifest.js +1 -0
  6. data/app/assets/stylesheets/db_blaster/application.css +15 -0
  7. data/app/controllers/db_blaster/application_controller.rb +6 -0
  8. data/app/helpers/db_blaster/application_helper.rb +7 -0
  9. data/app/jobs/db_blaster/application_job.rb +7 -0
  10. data/app/jobs/db_blaster/publish_all_job.rb +20 -0
  11. data/app/jobs/db_blaster/publish_source_table_job.rb +16 -0
  12. data/app/models/concerns/db_blaster/sync.rb +21 -0
  13. data/app/models/db_blaster/application_record.rb +8 -0
  14. data/app/models/db_blaster/source_table.rb +10 -0
  15. data/config/brakeman.ignore +26 -0
  16. data/config/routes.rb +4 -0
  17. data/db/migrate/20210727222252_create_source_tables.rb +14 -0
  18. data/lib/db_blaster.rb +28 -0
  19. data/lib/db_blaster/available_tables.rb +12 -0
  20. data/lib/db_blaster/chunker.rb +77 -0
  21. data/lib/db_blaster/configuration.rb +58 -0
  22. data/lib/db_blaster/engine.rb +12 -0
  23. data/lib/db_blaster/finder.rb +70 -0
  24. data/lib/db_blaster/one_record_too_large_error.rb +30 -0
  25. data/lib/db_blaster/publish_source_table.rb +30 -0
  26. data/lib/db_blaster/publisher.rb +42 -0
  27. data/lib/db_blaster/rspec.rb +9 -0
  28. data/lib/db_blaster/source_table_configuration.rb +19 -0
  29. data/lib/db_blaster/source_table_configuration_builder.rb +55 -0
  30. data/lib/db_blaster/version.rb +5 -0
  31. data/lib/generators/db_blaster/install/install_generator.rb +15 -0
  32. data/lib/generators/db_blaster/install/templates/db_blaster_config.rb +37 -0
  33. data/lib/tasks/db_blaster_tasks.rake +5 -0
  34. metadata +162 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e8d31a519addfa74e6a2ca3c2d41f58b05e2b4551802948b5a4dc0fe20278dca
4
+ data.tar.gz: 33a1e76574a2b8e70c681beebf544af587ff143252b08d0dd1a8dbc0b3615864
5
+ SHA512:
6
+ metadata.gz: 378969c9223acf49c50a055b566b1f3f2bff3a0b032bbc9e1aebbb64b5031c9d1a2af6c539d55efc7e90a8254d0ca4cf413be762ab41dbdd1fd4e01efd723d33
7
+ data.tar.gz: afabd1c77490c8c80bdd1d0a49965026ae69a77d727ffe7da475c5683391dafc4cd9f91d7880ecc5c0818a4c5afeacd4403844a198f932c0c4d448b0c53cb2dc
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Perry Hertler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/perryqh/be2fa5413124206272dbc700f3201f5a/raw/db_blaster__master.json)
2
+ # DbBlaster
3
+ ![Image of DB to SNS](https://lucid.app/publicSegments/view/c70feed3-2f48-46ee-8734-423474488feb/image.png)
4
+
5
+ DbBlaster publishes changed database rows to AWS SNS. The first time `DbBlaster::PublishAllJob.perform_later` is ran,
6
+ the entire database will be incrementally published to SNS. Subsequent runs will publish rows whose `updated_at` column
7
+ is more recent than the last run.
8
+
9
+ Consuming the published messages is functionality not provided by DbBlaster.
10
+
11
+ ## Usage
12
+
13
+ Update `config/initializers/db_blaster_config.rb` with valid AWS credentials, topics, and options.
14
+
15
+ Schedule `DbBlaster::PublishAllJob.perform_later` to run periodically with something
16
+ like [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) or [whenever](https://github.com/javan/whenever)
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'db_blaster'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```bash
29
+ $ bundle
30
+ ```
31
+
32
+ Install Migrations:
33
+
34
+ ```bash
35
+ $ rake db_blaster:install:migrations && rake db:migrate
36
+ ```
37
+
38
+ Copy sample config file to rails project:
39
+
40
+ ```bash
41
+ rails g db_blaster:install
42
+ ```
43
+
44
+ ## License
45
+
46
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
11
+
12
+ begin
13
+ require 'rspec/core/rake_task'
14
+
15
+ RSpec::Core::RakeTask.new(:spec)
16
+
17
+ task default: :spec
18
+ rescue LoadError
19
+ # no rspec available
20
+ end
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/db_blaster .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Helper methods for views
5
+ module ApplicationHelper
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Base ActiveJob
5
+ class ApplicationJob < ActiveJob::Base
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveJob
4
+ # 1) sync configuration with DbBlaster::SourceTable
5
+ # 2) Enqueue PublishSourceTableJob for every source_table
6
+ module DbBlaster
7
+ # Enqueues PublishSourceTableJob for every source-table
8
+ class PublishAllJob < ApplicationJob
9
+ queue_as 'default'
10
+
11
+ def perform
12
+ SourceTable.sync(SourceTableConfigurationBuilder
13
+ .build_all(DbBlaster.configuration))
14
+
15
+ DbBlaster::SourceTable.pluck(:id).each do |source_table_id|
16
+ PublishSourceTableJob.perform_later(source_table_id)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Executes PublishSourceTable for provided `source_table_id`
4
+ module DbBlaster
5
+ # Publishes changed rows to SNS
6
+ class PublishSourceTableJob < ApplicationJob
7
+ queue_as 'default'
8
+
9
+ def perform(source_table_id)
10
+ source_table = SourceTable.find_by(id: source_table_id)
11
+ return unless source_table
12
+
13
+ PublishSourceTable.execute(source_table)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Syncs tables derived from SourceTableConfigurationBuilder
5
+ # with SourceTable rows
6
+ module Sync
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ # Syncs configuration tables with db
11
+ def sync(table_configs)
12
+ SourceTable.where.not(name: table_configs.collect(&:source_table_name)).delete_all
13
+
14
+ table_configs.each do |config|
15
+ source_table = SourceTable.where(name: config.source_table_name).first_or_create
16
+ source_table.update!(config.update_params)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Base class for models
5
+ class ApplicationRecord < ActiveRecord::Base
6
+ self.abstract_class = true
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Represents tables that should be synced
5
+ class SourceTable < ApplicationRecord
6
+ include Sync
7
+
8
+ validates :name, uniqueness: { case_sensitive: false }, presence: true
9
+ end
10
+ end
@@ -0,0 +1,26 @@
1
+ {
2
+ "ignored_warnings": [
3
+ {
4
+ "warning_type": "SQL Injection",
5
+ "warning_code": 0,
6
+ "fingerprint": "6f4d3da0707c3f5f5c5bf5a002a254fee246210248aafa655cb2f15adfb47aa7",
7
+ "check_name": "SQL",
8
+ "message": "Possible SQL injection",
9
+ "file": "lib/db_blaster/finder.rb",
10
+ "line": 38,
11
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
12
+ "code": "ActiveRecord::Base.connection.execute(\"#{select_sql} OFFSET #{offset}\")",
13
+ "render_path": null,
14
+ "location": {
15
+ "type": "method",
16
+ "class": "DbBlaster::Finder",
17
+ "method": "find_records_in_batches"
18
+ },
19
+ "user_input": "select_sql",
20
+ "confidence": "Medium",
21
+ "note": "No SQL injection can occur"
22
+ }
23
+ ],
24
+ "updated": "2021-08-09 11:03:06 -0600",
25
+ "brakeman_version": "5.1.1"
26
+ }
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DbBlaster::Engine.routes.draw do
4
+ # end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Create db_blaster_source_tables
4
+ class CreateSourceTables < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :db_blaster_source_tables do |t|
7
+ t.string :name, null: false
8
+ t.text :ignored_columns, array: true, default: []
9
+ t.datetime :last_published_updated_at # the most recent published `updated_at`
10
+ t.integer :batch_size, default: 100
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
data/lib/db_blaster.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+ require 'db_blaster/version'
5
+ require 'db_blaster/engine'
6
+ require 'db_blaster/one_record_too_large_error'
7
+ require 'db_blaster/available_tables'
8
+ require 'db_blaster/configuration'
9
+ require 'db_blaster/source_table_configuration'
10
+ require 'db_blaster/source_table_configuration_builder'
11
+ require 'db_blaster/publisher'
12
+ require 'db_blaster/publish_source_table'
13
+ require 'db_blaster/chunker'
14
+ require 'db_blaster/finder'
15
+
16
+ # Top-level module that serves as an entry point
17
+ # into the engine gem
18
+ module DbBlaster
19
+ class << self
20
+ def configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ def configure
25
+ yield(configuration)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Finds all tables in current database and removes the "system tables"
5
+ module AvailableTables
6
+ SYSTEM_TABLES = %w[schema_migrations ar_internal_metadata].freeze
7
+
8
+ def available_tables
9
+ @available_tables ||= ActiveRecord::Base.connection.tables - SYSTEM_TABLES
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Chunk the records into sizes < Configuration.max_message_size_in_kilobytes
5
+ # yielding the chunks inline to the provided block
6
+ # If the records' size is already less than Configuration.max_message_size_in_kilobytes,
7
+ # all the records are yielded to the provided block
8
+ class Chunker
9
+ attr_reader :source_table, :records, :block_on_chunk, :current_chunk, :current_chunk_size
10
+
11
+ def initialize(source_table, records, &block_on_chunk)
12
+ @source_table = source_table
13
+ @records = records
14
+ @block_on_chunk = block_on_chunk
15
+ @current_chunk_size = 0
16
+ @current_chunk = []
17
+ end
18
+
19
+ def self.chunk(source_table, records, &block)
20
+ new(source_table, records, &block).chunk
21
+ end
22
+
23
+ def chunk
24
+ return if yield_if_records_acceptable?
25
+
26
+ breakup_records
27
+ end
28
+
29
+ def yield_if_records_acceptable?
30
+ return if records.to_json.size >= max_bytes
31
+
32
+ block_on_chunk.call(records)
33
+ true
34
+ end
35
+
36
+ def max_bytes
37
+ @max_bytes ||= 1000 * max_kilobytes
38
+ end
39
+
40
+ def max_kilobytes
41
+ DbBlaster.configuration.max_message_size_in_kilobytes ||
42
+ DbBlaster.configuration.class::DEFAULT_MAX_MESSAGE_SIZE_IN_KILOBYTES
43
+ end
44
+
45
+ def breakup_records
46
+ records.each(&method(:process_record))
47
+ block_on_chunk.call(current_chunk) if current_chunk.length.positive?
48
+ end
49
+
50
+ private
51
+
52
+ def process_record(record)
53
+ record_size = record.to_json.size
54
+ @current_chunk_size += record_size
55
+ if current_chunk_size < max_bytes
56
+ current_chunk << record
57
+ elsif record_size >= max_bytes
58
+ blow_up_one_record_too_large(record)
59
+ else
60
+ yield_chunk(record, record_size)
61
+ end
62
+ end
63
+
64
+ def yield_chunk(record, record_size)
65
+ block_on_chunk.call(current_chunk)
66
+ @current_chunk_size = record_size
67
+ @current_chunk = [record]
68
+ end
69
+
70
+ def blow_up_one_record_too_large(record)
71
+ block_on_chunk.call(current_chunk) if current_chunk.length.positive?
72
+ raise OneRecordTooLargeError.new(source_table: source_table,
73
+ record: record,
74
+ max_kilobytes: max_kilobytes)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Configuration class for providing credentials, topics, and customizations.
5
+ class Configuration
6
+ DEFAULT_BATCH_SIZE = 100
7
+ DEFAULT_MAX_MESSAGE_SIZE_IN_KILOBYTES = 256 # max size allowed by AWS SNS
8
+
9
+ # The required configuration fields
10
+ REQUIRED_FIELDS = %i[aws_access_key aws_access_secret aws_region sns_topic].freeze
11
+
12
+ # The topic to which messages will be published
13
+ attr_accessor :sns_topic
14
+ attr_accessor :aws_access_key, :aws_access_secret, :aws_region
15
+
16
+ # Global list of column names not to include in published SNS messages
17
+ # example: config.ignored_column_names = ['email', 'phone_number']
18
+ attr_accessor :ignored_column_names
19
+
20
+ # Optional
21
+ # If set, only publish tables specified.
22
+ # example: config.only_source_tables = ['posts', 'tags', 'comments']
23
+ attr_accessor :only_source_tables
24
+
25
+ # Optional
26
+ # Customize batch_size and/or ignored_columns
27
+ # example:
28
+ # config.source_table_options = [{ source_table_name: 'posts', batch_size: 100, ignored_column_names: ['email'] },
29
+ # { source_table_name: 'comments', ignored_column_names: ['tags'] }]
30
+ attr_accessor :source_table_options
31
+
32
+ # Optional
33
+ # Extra [SNS message_attributes](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
34
+ # Attributes set here will be included in every published message
35
+ # example: config.extra_sns_message_attributes = {'infra_id' => {data_type: 'String', value: '061'}}
36
+ attr_accessor :extra_sns_message_attributes
37
+
38
+ # Optional
39
+ # db_blaster will select and then publish `batch_size` rows at a time
40
+ # Default value is 100
41
+ attr_accessor :batch_size
42
+
43
+ # Optional
44
+ # DbBlaster will publish no messages larger than this value
45
+ # Default value is 256
46
+ attr_accessor :max_message_size_in_kilobytes
47
+
48
+ # Raises error if a required field is not set
49
+ def verify!
50
+ no_values = REQUIRED_FIELDS.select do |attribute|
51
+ send(attribute).nil? || send(attribute).strip.empty?
52
+ end
53
+ return if no_values.empty?
54
+
55
+ raise "missing configuration values for [#{no_values.join(', ')}]"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Engine to isolate DbBlaster namespace
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace DbBlaster
7
+
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Find records and yield them a `batch_size` at a time
5
+ class Finder
6
+ include AvailableTables
7
+ attr_reader :source_table, :block_on_find, :offset
8
+
9
+ def initialize(source_table, &block)
10
+ @source_table = source_table
11
+ @block_on_find = block
12
+ @offset = 0
13
+ end
14
+
15
+ delegate :batch_size, :name, :last_published_updated_at, to: :source_table, prefix: true
16
+
17
+ def self.find(source_table, &block)
18
+ new(source_table, &block).find
19
+ end
20
+
21
+ def find
22
+ verify_source_table_name
23
+
24
+ find_records_in_batches do |batch|
25
+ filtered = batch.collect(&method(:filter_columns))
26
+ next if filtered.blank?
27
+
28
+ Chunker.chunk(source_table, filtered) do |chunked|
29
+ block_on_find.call(chunked)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def find_records_in_batches
37
+ loop do
38
+ result = ActiveRecord::Base.connection.execute("#{select_sql} OFFSET #{offset}")
39
+ yield(result)
40
+ break if result.count != source_table_batch_size
41
+
42
+ @offset += source_table_batch_size
43
+ end
44
+ end
45
+
46
+ def filter_columns(selected_row)
47
+ selected_row.except(*source_table.ignored_columns)
48
+ end
49
+
50
+ def verify_source_table_name
51
+ raise invalid_source_table_message unless available_tables.include?(source_table_name)
52
+ end
53
+
54
+ def invalid_source_table_message
55
+ "source_table.name: '#{source_table_name}' does not exist!"
56
+ end
57
+
58
+ def select_sql
59
+ "SELECT * FROM #{source_table_name} #{where} ORDER BY updated_at ASC LIMIT #{source_table_batch_size}"
60
+ end
61
+
62
+ def where
63
+ return '' unless source_table_last_published_updated_at
64
+
65
+ ActiveRecord::Base.sanitize_sql_for_conditions(
66
+ ['WHERE updated_at >= :updated_at', { updated_at: source_table_last_published_updated_at.to_s(:db) }]
67
+ )
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Custom error representing a serious error.
5
+ # If one individual record is larger than the max bytes
6
+ # allowed, db_blaster "fails fast".
7
+ # If we ignored this record and moved on, it would never make it
8
+ # to SNS as its `updated_at` would end up being less than the
9
+ # last processed record.
10
+ # Possible fixes:
11
+ # 1) if possible ignore the column(s) that are too large
12
+ # 2) manually move the record to the intended destination
13
+ # 3) bug me to provide an S3 workaround
14
+ class OneRecordTooLargeError < StandardError
15
+ attr_reader :source_table, :record, :max_kilobytes
16
+
17
+ def initialize(params)
18
+ super
19
+ params.each_key do |key|
20
+ instance_variable_set("@#{key}", params[key])
21
+ end
22
+ end
23
+
24
+ def message
25
+ ["One individual record with ID '#{record[:id]}'",
26
+ " in source-table '#{source_table.name}'",
27
+ " is larger than #{max_kilobytes} KB!"].join(' ')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Given a `source_table` providing the table name,
5
+ # finds rows in `batch_size` chunks that are published to SNS
6
+ class PublishSourceTable
7
+ attr_reader :source_table
8
+
9
+ def initialize(source_table)
10
+ @source_table = source_table
11
+ end
12
+
13
+ def self.execute(source_table)
14
+ new(source_table).execute
15
+ end
16
+
17
+ def execute
18
+ DbBlaster.configuration.verify! # will raise error if required configurations are not set
19
+
20
+ # pessimistically lock row for the duration
21
+ source_table.with_lock do
22
+ Finder.find(source_table) do |records|
23
+ Publisher.publish(source_table, records)
24
+ source_table.update(last_published_updated_at: records.last['updated_at'])
25
+ end
26
+ end
27
+ self
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # publish records to SNS topic
4
+ module DbBlaster
5
+ # Publishes records to AWS SNS
6
+ class Publisher
7
+ attr_reader :source_table, :records
8
+
9
+ def initialize(source_table, records)
10
+ @source_table = source_table
11
+ @records = records
12
+ end
13
+
14
+ def self.publish(source_table, records)
15
+ new(source_table, records).publish
16
+ end
17
+
18
+ def publish
19
+ topic.publish(message_attributes: message_attributes,
20
+ message: records.to_json)
21
+ end
22
+
23
+ def topic
24
+ resource.topic(DbBlaster.configuration.sns_topic)
25
+ end
26
+
27
+ def resource
28
+ @resource ||= Aws::SNS::Resource.new(client: client)
29
+ end
30
+
31
+ def client
32
+ @client ||= Aws::SNS::Client.new(region: DbBlaster.configuration.aws_region,
33
+ credentials: Aws::Credentials.new(DbBlaster.configuration.aws_access_key,
34
+ DbBlaster.configuration.aws_access_secret))
35
+ end
36
+
37
+ def message_attributes
38
+ (DbBlaster.configuration.extra_sns_message_attributes || {})
39
+ .merge('source_table' => { data_type: 'String', string_value: source_table.name })
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(FactoryBot)
4
+ FactoryBot.define do
5
+ factory :db_blaster_source_table, class: DbBlaster::SourceTable do
6
+ sequence(:name) { |n| "table_#{n}" }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # PORO for source-table-configuration fields
5
+ class SourceTableConfiguration
6
+ attr_reader :source_table_name, :batch_size, :ignored_column_names
7
+
8
+ def initialize(params)
9
+ params.each_key do |key|
10
+ instance_variable_set("@#{key}", params[key])
11
+ end
12
+ end
13
+
14
+ def update_params
15
+ { batch_size: batch_size,
16
+ ignored_columns: ignored_column_names }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ # Builds source-table configurations based off the tables in the
5
+ # current database and the provided DbBlaster::Configuration
6
+ class SourceTableConfigurationBuilder
7
+ include AvailableTables
8
+ attr_reader :configuration, :source_table_configurations
9
+
10
+ def initialize(configuration)
11
+ @configuration = configuration
12
+ @source_table_configurations = []
13
+ end
14
+
15
+ def self.build_all(configuration)
16
+ new(configuration).build_all
17
+ end
18
+
19
+ def build_all
20
+ @build_all ||= table_names_for_configuration
21
+ .collect(&method(:build_configuration))
22
+ end
23
+
24
+ def build_configuration(source_table_name)
25
+ SourceTableConfiguration.new(source_table_name: source_table_name,
26
+ batch_size: batch_size(source_table_name),
27
+ ignored_column_names: ignored_column_names(source_table_name))
28
+ end
29
+
30
+ def batch_size(source_table_name)
31
+ overridden_value_or_global(source_table_name, :batch_size) || configuration.class::DEFAULT_BATCH_SIZE
32
+ end
33
+
34
+ def ignored_column_names(source_table_name)
35
+ overridden_value_or_global(source_table_name, :ignored_column_names) || []
36
+ end
37
+
38
+ def overridden_value_or_global(source_table_name, field_name)
39
+ find_source_table_options(source_table_name)&.send(:[], field_name) || configuration.send(field_name)
40
+ end
41
+
42
+ def find_source_table_options(source_table_name)
43
+ (configuration.source_table_options || [])
44
+ .detect { |option| option[:source_table_name] == source_table_name }
45
+ end
46
+
47
+ def table_names_for_configuration
48
+ if configuration.only_source_tables&.length&.positive?
49
+ available_tables & configuration.only_source_tables
50
+ else
51
+ available_tables
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbBlaster
4
+ module Generators
5
+ # Generator to copy a sample db_blaster_config.rb to the
6
+ # rails app's config/initializer directory
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def copy_initializer_file
11
+ copy_file 'db_blaster_config.rb', 'config/initializers/db_blaster_config.rb'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ DbBlaster.configure do |config|
4
+ # SNS topic to receive database changes
5
+ config.sns_topic = 'the-topic'
6
+ config.aws_access_key = 'access-key'
7
+ config.aws_access_secret = 'secret'
8
+ config.aws_region = 'region'
9
+
10
+ # Optional
11
+ # db_blaster will select and then publish `batch_size` rows at a time
12
+ # config.batch_size = 100
13
+
14
+ # Optional
15
+ # db_blaster will publish no messages larger than this value
16
+ # Default value is 256
17
+ # attr_accessor :max_message_size_in_kilobytes
18
+
19
+ # Optional
20
+ # Extra [SNS message_attributes](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
21
+ # Attributes set here will be included in every published message
22
+ # config.extra_sns_message_attributes = {'infra_id' => {data_type: 'String', value: '061'}}
23
+
24
+ # Global list of column names not to include in published SNS messages
25
+ # example: config.ignored_column_names = ['email', 'phone_number']
26
+ # config.ignored_column_names = ['email', 'phone_number']
27
+
28
+ # Optional
29
+ # If set, only publish tables specified.
30
+ # config.only_source_tables = ['posts', 'tags', 'comments']
31
+
32
+ # Optional
33
+ # Customize batch_size and/or ignored_columns
34
+ # example:
35
+ # config.source_table_options = [{ source_table_name: 'posts', batch_size: 100, ignored_column_names: ['email'] },
36
+ # { source_table_name: 'comments', ignored_column_names: ['tags'] }]
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :db_blaster do
4
+ # # Task goes here
5
+ # end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db_blaster
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Perry Hertler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-sns
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: factory_bot_rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-its
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
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: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
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: shoulda-matchers
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Push db changes to AWS SNS.
98
+ email:
99
+ - perry@hertler.org
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - MIT-LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - app/assets/config/db_blaster_manifest.js
108
+ - app/assets/stylesheets/db_blaster/application.css
109
+ - app/controllers/db_blaster/application_controller.rb
110
+ - app/helpers/db_blaster/application_helper.rb
111
+ - app/jobs/db_blaster/application_job.rb
112
+ - app/jobs/db_blaster/publish_all_job.rb
113
+ - app/jobs/db_blaster/publish_source_table_job.rb
114
+ - app/models/concerns/db_blaster/sync.rb
115
+ - app/models/db_blaster/application_record.rb
116
+ - app/models/db_blaster/source_table.rb
117
+ - config/brakeman.ignore
118
+ - config/routes.rb
119
+ - db/migrate/20210727222252_create_source_tables.rb
120
+ - lib/db_blaster.rb
121
+ - lib/db_blaster/available_tables.rb
122
+ - lib/db_blaster/chunker.rb
123
+ - lib/db_blaster/configuration.rb
124
+ - lib/db_blaster/engine.rb
125
+ - lib/db_blaster/finder.rb
126
+ - lib/db_blaster/one_record_too_large_error.rb
127
+ - lib/db_blaster/publish_source_table.rb
128
+ - lib/db_blaster/publisher.rb
129
+ - lib/db_blaster/rspec.rb
130
+ - lib/db_blaster/source_table_configuration.rb
131
+ - lib/db_blaster/source_table_configuration_builder.rb
132
+ - lib/db_blaster/version.rb
133
+ - lib/generators/db_blaster/install/install_generator.rb
134
+ - lib/generators/db_blaster/install/templates/db_blaster_config.rb
135
+ - lib/tasks/db_blaster_tasks.rake
136
+ homepage: https://github.com/perryqh/db_blaster
137
+ licenses:
138
+ - MIT
139
+ metadata:
140
+ homepage_uri: https://github.com/perryqh/db_blaster
141
+ source_code_uri: https://github.com/perryqh/db_blaster
142
+ changelog_uri: https://github.com/perryqh/db_blaster/CHANGELOG.md
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '2.6'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubygems_version: 3.1.6
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Push db changes to AWS SNS.
162
+ test_files: []