active_record_streams 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.github/PULL_REQUEST_TEMPLATE.md +35 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +5 -0
  5. data/.rubocop.yml +10 -0
  6. data/.travis.yml +12 -0
  7. data/CHANGELOG.md +13 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +96 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +168 -0
  12. data/Rakefile +8 -0
  13. data/active_record_streams.gemspec +55 -0
  14. data/lib/active_record_streams.rb +26 -0
  15. data/lib/active_record_streams/config.rb +15 -0
  16. data/lib/active_record_streams/config_spec.rb +32 -0
  17. data/lib/active_record_streams/credentials.rb +31 -0
  18. data/lib/active_record_streams/credentials_spec.rb +79 -0
  19. data/lib/active_record_streams/extensions/active_record/base.rb +41 -0
  20. data/lib/active_record_streams/extensions/active_record/base_spec.rb +45 -0
  21. data/lib/active_record_streams/extensions/active_record/persistence.rb +17 -0
  22. data/lib/active_record_streams/extensions/active_record/persistence_spec.rb +36 -0
  23. data/lib/active_record_streams/extensions/active_record/relation.rb +60 -0
  24. data/lib/active_record_streams/extensions/active_record/relation_spec.rb +41 -0
  25. data/lib/active_record_streams/message.rb +21 -0
  26. data/lib/active_record_streams/message_spec.rb +29 -0
  27. data/lib/active_record_streams/publishers/http_stream.rb +58 -0
  28. data/lib/active_record_streams/publishers/http_stream_spec.rb +73 -0
  29. data/lib/active_record_streams/publishers/kinesis_client.rb +26 -0
  30. data/lib/active_record_streams/publishers/kinesis_client_spec.rb +49 -0
  31. data/lib/active_record_streams/publishers/kinesis_stream.rb +64 -0
  32. data/lib/active_record_streams/publishers/kinesis_stream_spec.rb +93 -0
  33. data/lib/active_record_streams/publishers/sns_client.rb +25 -0
  34. data/lib/active_record_streams/publishers/sns_client_spec.rb +46 -0
  35. data/lib/active_record_streams/publishers/sns_stream.rb +52 -0
  36. data/lib/active_record_streams/publishers/sns_stream_spec.rb +89 -0
  37. data/lib/active_record_streams/version.rb +5 -0
  38. data/lib/active_record_streams/version_spec.rb +9 -0
  39. data/lib/active_record_streams_spec.rb +21 -0
  40. metadata +199 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record_streams/version'
4
+ require 'active_record_streams/config'
5
+ require 'active_record_streams/message'
6
+ require 'active_record_streams/credentials'
7
+
8
+ require 'active_record_streams/publishers/kinesis_client'
9
+ require 'active_record_streams/publishers/kinesis_stream'
10
+ require 'active_record_streams/publishers/sns_client'
11
+ require 'active_record_streams/publishers/sns_stream'
12
+ require 'active_record_streams/publishers/http_stream'
13
+
14
+ require 'active_record_streams/extensions/active_record/persistence'
15
+ require 'active_record_streams/extensions/active_record/relation'
16
+ require 'active_record_streams/extensions/active_record/base'
17
+
18
+ module ActiveRecordStreams
19
+ def self.configure
20
+ yield config
21
+ end
22
+
23
+ def self.config
24
+ @config ||= Config.new
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordStreams
4
+ class Config
5
+ attr_accessor :aws_region, :aws_access_key_id, :aws_secret_access_key,
6
+ :streams
7
+
8
+ def initialize
9
+ @aws_region = nil
10
+ @aws_access_key_id = nil
11
+ @aws_secret_access_key = nil
12
+ @streams = []
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+
5
+ RSpec.describe ActiveRecordStreams::Config do
6
+ describe 'attr accessors' do
7
+ context 'initial values' do
8
+ it 'returns correct initial values' do
9
+ expect(subject.aws_region).to eq(nil)
10
+ expect(subject.aws_access_key_id).to eq(nil)
11
+ expect(subject.aws_secret_access_key).to eq(nil)
12
+ expect(subject.streams).to eq([])
13
+ end
14
+ end
15
+
16
+ context 'attr setters' do
17
+ it 'sets and returns proper values' do
18
+ subject.aws_region = 'test-region'
19
+ expect(subject.aws_region).to eq('test-region')
20
+
21
+ subject.aws_access_key_id = 'test-access-key-id'
22
+ expect(subject.aws_access_key_id).to eq('test-access-key-id')
23
+
24
+ subject.aws_secret_access_key = 'test-secret-access-key'
25
+ expect(subject.aws_secret_access_key).to eq('test-secret-access-key')
26
+
27
+ subject.streams << 'test-stream'
28
+ expect(subject.streams).to eq(['test-stream'])
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordStreams
4
+ class Credentials
5
+ def build_credentials
6
+ region_config
7
+ .merge(access_key_id_config)
8
+ .merge(secret_access_key_config)
9
+ end
10
+
11
+ private
12
+
13
+ def region_config
14
+ return {} unless ActiveRecordStreams.config.aws_region
15
+
16
+ { region: ActiveRecordStreams.config.aws_region }
17
+ end
18
+
19
+ def access_key_id_config
20
+ return {} unless ActiveRecordStreams.config.aws_access_key_id
21
+
22
+ { access_key_id: ActiveRecordStreams.config.aws_access_key_id }
23
+ end
24
+
25
+ def secret_access_key_config
26
+ return {} unless ActiveRecordStreams.config.aws_secret_access_key
27
+
28
+ { secret_access_key: ActiveRecordStreams.config.aws_secret_access_key }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'credentials'
4
+
5
+ RSpec.describe ActiveRecordStreams::Credentials do
6
+ describe '#build_credentials' do
7
+ let(:config) do
8
+ double(
9
+ aws_region: nil,
10
+ aws_access_key_id: nil,
11
+ aws_secret_access_key: nil
12
+ )
13
+ end
14
+
15
+ before do
16
+ allow(ActiveRecordStreams).to receive(:config).and_return(config)
17
+ end
18
+
19
+ subject { described_class.new.build_credentials }
20
+
21
+ context 'has region config' do
22
+ before do
23
+ allow(config).to receive(:aws_region).and_return('eu-central-1')
24
+ end
25
+
26
+ it 'returns a proper config' do
27
+ expect(subject).to eq(region: 'eu-central-1')
28
+ end
29
+ end
30
+
31
+ context 'has no region config' do
32
+ it 'returns a proper config' do
33
+ expect(subject).to eq({})
34
+ end
35
+ end
36
+
37
+ context 'has access keys config' do
38
+ before do
39
+ allow(config).to receive(:aws_access_key_id).and_return('access-key-id')
40
+ allow(config).to receive(:aws_secret_access_key).and_return('secret')
41
+ end
42
+
43
+ it 'returns a proper config' do
44
+ expect(subject).to eq(
45
+ access_key_id: 'access-key-id',
46
+ secret_access_key: 'secret'
47
+ )
48
+ end
49
+ end
50
+
51
+ context 'has no access keys config' do
52
+ it 'returns a proper config' do
53
+ expect(subject).to eq({})
54
+ end
55
+ end
56
+
57
+ context 'has region and access key config' do
58
+ before do
59
+ allow(config).to receive(:aws_region).and_return('eu-central-1')
60
+ allow(config).to receive(:aws_access_key_id).and_return('access-key-id')
61
+ allow(config).to receive(:aws_secret_access_key).and_return('secret')
62
+ end
63
+
64
+ it 'returns a proper config' do
65
+ expect(subject).to eq(
66
+ region: 'eu-central-1',
67
+ access_key_id: 'access-key-id',
68
+ secret_access_key: 'secret'
69
+ )
70
+ end
71
+ end
72
+
73
+ context 'has no any config' do
74
+ it 'returns a proper config' do
75
+ expect(subject).to eq({})
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ Base.prepend(
5
+ Module.new do
6
+ def self.prepended(object)
7
+ object.class_exec do
8
+ before_save :capture_old_image
9
+ after_commit :publish_event_to_streams
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def capture_old_image
16
+ @old_image = attributes.keys.map do |attribute|
17
+ [attribute, attribute_was(attribute)]
18
+ end.to_h
19
+ end
20
+
21
+ def publish_event_to_streams
22
+ ActiveRecordStreams.config.streams.each do |stream|
23
+ stream.publish(self.class.table_name, stream_message)
24
+ end
25
+ end
26
+
27
+ def stream_message
28
+ ::ActiveRecordStreams::Message.new(
29
+ self.class.table_name, stream_action_type, @old_image, attributes
30
+ )
31
+ end
32
+
33
+ def stream_action_type
34
+ return :create if transaction_include_any_action?([:create])
35
+ return :destroy if transaction_include_any_action?([:destroy])
36
+
37
+ :update
38
+ end
39
+ end
40
+ )
41
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../spec/mocks/active_record/base'
4
+ require_relative 'base'
5
+
6
+ RSpec.describe ActiveRecord::Base do
7
+ let(:message) { double(new: nil) }
8
+ let(:stream) { double(publish: nil) }
9
+ let(:streams_config) { double(streams: [stream]) }
10
+
11
+ before do
12
+ allow(ActiveRecordStreams).to receive(:config).and_return(streams_config)
13
+ end
14
+
15
+ describe 'persistence' do
16
+ let(:old_image) do
17
+ {
18
+ 'hello' => 'old world',
19
+ 'world' => 'old hello'
20
+ }
21
+ end
22
+
23
+ let(:new_image) do
24
+ {
25
+ 'hello' => 'world',
26
+ 'world' => 'hello'
27
+ }
28
+ end
29
+
30
+ before do
31
+ allow(ActiveRecordStreams::Message)
32
+ .to receive(:new)
33
+ .with(described_class.table_name, :create, old_image, new_image)
34
+ .and_return(message)
35
+ end
36
+
37
+ it 'publishes event to streams with proper data' do
38
+ expect(stream)
39
+ .to receive(:publish)
40
+ .with(described_class.table_name, message)
41
+
42
+ subject._invoke_persistent_callbacks
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ Persistence.prepend(
5
+ Module.new do
6
+ private
7
+
8
+ ##
9
+ # destroy/destroy! methods should still call `delete_all` without
10
+ # callbacks, since they already serve their own callbacks
11
+
12
+ def destroy_row
13
+ relation_for_destroy.delete_all_without_callbacks
14
+ end
15
+ end
16
+ )
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../spec/mocks/active_record/persistence'
4
+ require_relative 'persistence'
5
+
6
+ RSpec.describe ActiveRecord::Persistence do
7
+ let(:relation_for_destroy) { double }
8
+
9
+ before do
10
+ allow(subject)
11
+ .to receive(:relation_for_destroy)
12
+ .and_return(relation_for_destroy)
13
+ end
14
+
15
+ subject do
16
+ Class.new do
17
+ include ActiveRecord::Persistence
18
+ end.new
19
+ end
20
+
21
+ describe 'destroy!' do
22
+ it 'calls original delete_all' do
23
+ expect(relation_for_destroy).to receive(:delete_all_without_callbacks)
24
+
25
+ subject.destroy!
26
+ end
27
+ end
28
+
29
+ describe 'destroy' do
30
+ it 'calls original delete_all' do
31
+ expect(relation_for_destroy).to receive(:delete_all_without_callbacks)
32
+
33
+ subject.destroy
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ Relation.prepend(
5
+ Module.new do
6
+ def self.prepended(object)
7
+ object.class_eval do
8
+ alias_method :delete_all_without_callbacks, :delete_all
9
+ alias_method :delete_all, :delete_all_with_callbacks
10
+ end
11
+ end
12
+
13
+ def update_all(updates)
14
+ find_each.inject(0) do |counter, record|
15
+ record.assign_attributes(updates)
16
+ counter += 1 if record.save(validate: false)
17
+
18
+ counter
19
+ end
20
+ end
21
+
22
+ ##
23
+ # Override `delete_all` method to serve callbacks.
24
+ # Change to this method also affects `delete` method.
25
+
26
+ # rubocop:disable Metrics/MethodLength
27
+ def delete_all_with_callbacks(conditions = nil)
28
+ return where(conditions).delete_all if conditions
29
+
30
+ find_each.inject(0) do |count, record|
31
+ wrap_delete_with_callbacks(record) do
32
+ deleted_count = where(
33
+ primary_key => record.attributes[primary_key]
34
+ ).delete_all_without_callbacks
35
+
36
+ if deleted_count == 1
37
+ record.instance_variable_set('@destroyed', true)
38
+ count += 1
39
+ end
40
+
41
+ count
42
+ end
43
+ end
44
+ end
45
+ # rubocop:enable Metrics/MethodLength
46
+
47
+ private
48
+
49
+ def wrap_delete_with_callbacks(record)
50
+ record.run_callbacks(:commit) do
51
+ record.run_callbacks(:destroy) do
52
+ ActiveRecord::Base.transaction do
53
+ yield
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ )
60
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../spec/mocks/active_record/relation'
4
+ require_relative 'relation'
5
+
6
+ RSpec.describe ActiveRecord::Relation do
7
+ let(:record) { double(assign_attributes: nil, save: nil, run_callbacks: nil, attributes: { 'id' => 1 }) }
8
+
9
+ before do
10
+ allow(subject).to receive(:where).and_return(subject)
11
+ allow(subject).to receive(:find_each).and_return([record])
12
+ end
13
+
14
+ describe '#update_all' do
15
+ it 'invokes save for each record' do
16
+ expect(record).to receive(:assign_attributes).with(a: 'b')
17
+ expect(record).to receive(:save).with(validate: false)
18
+
19
+ subject.update_all(a: 'b')
20
+ end
21
+ end
22
+
23
+ describe '#delete_all' do
24
+ before do
25
+ allow(subject).to receive(:primary_key).and_return('id')
26
+ end
27
+
28
+ it 'performs deletion invoking the callbacks' do
29
+ expect(record).to receive(:run_callbacks).with(:commit).and_yield
30
+ expect(record).to receive(:run_callbacks).with(:destroy).and_yield
31
+ expect(ActiveRecord::Base).to receive(:transaction).and_yield
32
+
33
+ expect(subject).to receive(:where).with('id' => 1).and_return(subject)
34
+ expect(subject).to receive(:delete_all_without_callbacks).and_return(1)
35
+
36
+ expect(record).to receive(:instance_variable_set).with('@destroyed', true)
37
+
38
+ subject.delete_all
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordStreams
4
+ class Message
5
+ def initialize(table_name, action_type, old_image, new_image)
6
+ @table_name = table_name
7
+ @action_type = action_type
8
+ @old_image = old_image || new_image
9
+ @new_image = new_image
10
+ end
11
+
12
+ def json
13
+ {
14
+ TableName: @table_name,
15
+ ActionType: @action_type,
16
+ OldImage: @old_image,
17
+ NewImage: @new_image
18
+ }.to_json
19
+ end
20
+ end
21
+ end