active_record_streams 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.
- checksums.yaml +7 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +35 -0
- data/.gitignore +12 -0
- data/.rspec +5 -0
- data/.rubocop.yml +10 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +96 -0
- data/MIT-LICENSE +20 -0
- data/README.md +168 -0
- data/Rakefile +8 -0
- data/active_record_streams.gemspec +55 -0
- data/lib/active_record_streams.rb +26 -0
- data/lib/active_record_streams/config.rb +15 -0
- data/lib/active_record_streams/config_spec.rb +32 -0
- data/lib/active_record_streams/credentials.rb +31 -0
- data/lib/active_record_streams/credentials_spec.rb +79 -0
- data/lib/active_record_streams/extensions/active_record/base.rb +41 -0
- data/lib/active_record_streams/extensions/active_record/base_spec.rb +45 -0
- data/lib/active_record_streams/extensions/active_record/persistence.rb +17 -0
- data/lib/active_record_streams/extensions/active_record/persistence_spec.rb +36 -0
- data/lib/active_record_streams/extensions/active_record/relation.rb +60 -0
- data/lib/active_record_streams/extensions/active_record/relation_spec.rb +41 -0
- data/lib/active_record_streams/message.rb +21 -0
- data/lib/active_record_streams/message_spec.rb +29 -0
- data/lib/active_record_streams/publishers/http_stream.rb +58 -0
- data/lib/active_record_streams/publishers/http_stream_spec.rb +73 -0
- data/lib/active_record_streams/publishers/kinesis_client.rb +26 -0
- data/lib/active_record_streams/publishers/kinesis_client_spec.rb +49 -0
- data/lib/active_record_streams/publishers/kinesis_stream.rb +64 -0
- data/lib/active_record_streams/publishers/kinesis_stream_spec.rb +93 -0
- data/lib/active_record_streams/publishers/sns_client.rb +25 -0
- data/lib/active_record_streams/publishers/sns_client_spec.rb +46 -0
- data/lib/active_record_streams/publishers/sns_stream.rb +52 -0
- data/lib/active_record_streams/publishers/sns_stream_spec.rb +89 -0
- data/lib/active_record_streams/version.rb +5 -0
- data/lib/active_record_streams/version_spec.rb +9 -0
- data/lib/active_record_streams_spec.rb +21 -0
- 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
|