sandthorn_sequel_projection 0.0.1

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +3 -0
  3. data/.gitignore +23 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +9 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +130 -0
  11. data/Rakefile +16 -0
  12. data/lib/sandthorn_sequel_projection.rb +57 -0
  13. data/lib/sandthorn_sequel_projection/cursor.rb +39 -0
  14. data/lib/sandthorn_sequel_projection/errors.rb +14 -0
  15. data/lib/sandthorn_sequel_projection/event_handler.rb +49 -0
  16. data/lib/sandthorn_sequel_projection/event_handler_collection.rb +27 -0
  17. data/lib/sandthorn_sequel_projection/lock.rb +83 -0
  18. data/lib/sandthorn_sequel_projection/manifest.rb +10 -0
  19. data/lib/sandthorn_sequel_projection/processed_events_tracker.rb +94 -0
  20. data/lib/sandthorn_sequel_projection/projection.rb +65 -0
  21. data/lib/sandthorn_sequel_projection/runner.rb +62 -0
  22. data/lib/sandthorn_sequel_projection/tasks.rb +14 -0
  23. data/lib/sandthorn_sequel_projection/utilities.rb +5 -0
  24. data/lib/sandthorn_sequel_projection/utilities/core_extensions/array_wrap.rb +13 -0
  25. data/lib/sandthorn_sequel_projection/utilities/null_proc.rb +9 -0
  26. data/lib/sandthorn_sequel_projection/version.rb +3 -0
  27. data/sandthorn_sequel_projection.gemspec +37 -0
  28. data/spec/cursor_spec.rb +44 -0
  29. data/spec/event_handler_collection_spec.rb +39 -0
  30. data/spec/event_handler_spec.rb +82 -0
  31. data/spec/integration/projection_spec.rb +126 -0
  32. data/spec/lock_spec.rb +141 -0
  33. data/spec/processed_events_tracker_spec.rb +79 -0
  34. data/spec/projection_spec.rb +91 -0
  35. data/spec/runner_spec.rb +32 -0
  36. data/spec/spec_helper.rb +23 -0
  37. data/spec/support/mock_event_store.rb +39 -0
  38. data/spec/test_data/event_data.json +1876 -0
  39. data/spec/utilities/null_proc_spec.rb +14 -0
  40. metadata +262 -0
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ module SandthornSequelProjection
4
+
5
+ class MyProjection < Projection
6
+
7
+ migration("20150303-1") do |db_con|
8
+ db_con.create_table?(table_name) do
9
+ primary_key :id
10
+ String :aggregate_id
11
+ TrueClass :on_sale, default: false
12
+ end
13
+ end
14
+
15
+ define_event_handlers do |handlers|
16
+ handlers.define product_added: {
17
+ aggregate_type: "SandthornProduct",
18
+ event_name: "new"
19
+ }
20
+ handlers.define on_sale: {
21
+ aggregate_type: "SandthornProduct",
22
+ event_name: "product_on_sale"
23
+ }
24
+ handlers.define removed_from_sale: {
25
+ aggregate_type: "SandthornProduct",
26
+ event_names: ["removed_from_sale", "destroyed"]
27
+ }
28
+ end
29
+
30
+ def on_sale(event)
31
+ aggregate_id = event[:aggregate_id]
32
+ add_aggregate(aggregate_id)
33
+ table.where(aggregate_id: aggregate_id).update(on_sale: true)
34
+ end
35
+
36
+ def add_aggregate(aggregate_id)
37
+ db_connection.transaction do
38
+ exists = table.where(aggregate_id: aggregate_id).any?
39
+ return exists || table.insert(aggregate_id: aggregate_id)
40
+ end
41
+ end
42
+
43
+ def product_added(event)
44
+ add_aggregate(event[:aggregate_id])
45
+ end
46
+
47
+ def removed_from_sale(event)
48
+ aggregate_id = event[:aggregate_id]
49
+ table.where(aggregate_id: aggregate_id).update(on_sale: false)
50
+ end
51
+
52
+ def table
53
+ db_connection[table_name]
54
+ end
55
+
56
+ def table_name
57
+ :products_on_sale
58
+ end
59
+
60
+ def aggregates_on_sale_ids
61
+ table.where(on_sale: true).select_map(:aggregate_id)
62
+ end
63
+
64
+ def aggregate_ids
65
+ table.select_map(:aggregate_id)
66
+ end
67
+ end
68
+
69
+ describe MyProjection do
70
+
71
+ def db_connection
72
+ SandthornSequelProjection.configuration.db_connection
73
+ end
74
+
75
+ def table
76
+ db_connection[projection.table_name]
77
+ end
78
+
79
+ let(:projection) { MyProjection.new }
80
+
81
+ before do
82
+ Sandthorn.default_event_store = MockEventStore.with_data
83
+ end
84
+
85
+ describe "#migrate!" do
86
+ it "creates the wanted table" do
87
+ projection.migrate!
88
+ expect(table.all).to eq([])
89
+ end
90
+ end
91
+
92
+ describe "#update" do
93
+ before do
94
+ projection.migrate!
95
+ end
96
+ methods = [:product_added, :removed_from_sale]
97
+ methods.each do |method|
98
+ it "calls #{method}" do
99
+ expect(projection).to receive(method).at_least(:once).and_call_original
100
+ projection.update!
101
+ end
102
+ end
103
+
104
+ describe "data" do
105
+ before { projection.update! }
106
+ it "sets the on_sale boolean correctly" do
107
+ expected = %w[
108
+ ac1be457-b6b9-4dad-900b-acb400f810df
109
+ ]
110
+ expect(projection.aggregates_on_sale_ids).to eq(expected)
111
+ end
112
+
113
+ it "sets the correct last processed sequence number" do
114
+ expect(projection.last_processed_sequence_number).to eq(128)
115
+ end
116
+
117
+ let(:json_events) { JSON.parse(File.read("./spec/test_data/event_data.json"), symbolize_names: true) }
118
+ let(:aggregate_ids) { json_events.map{|event| event[:aggregate_id] }.uniq }
119
+
120
+ it "records all aggregate ids" do
121
+ expect(projection.aggregate_ids).to contain_exactly(*aggregate_ids)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
data/spec/lock_spec.rb ADDED
@@ -0,0 +1,141 @@
1
+ module SandthornSequelProjection
2
+
3
+ describe Lock do
4
+ let(:db_connection) { Sequel.sqlite }
5
+ let(:table_name) { ProcessedEventsTracker::DEFAULT_TABLE_NAME }
6
+ let(:lock) { SandthornSequelProjection::Lock.new("foo", db_connection) }
7
+
8
+ before(:each) do
9
+ db_connection.create_table?(table_name) do
10
+ String :identifier
11
+ DateTime :locked_at, null: true
12
+ end
13
+ db_connection[table_name].insert(identifier: lock.identifier)
14
+ end
15
+
16
+ def null_lock
17
+ db_connection[table_name].where(identifier: lock.identifier).update(locked_at: nil)
18
+ end
19
+
20
+ def set_lock(time = Time.now)
21
+ db_connection[table_name].where(identifier: lock.identifier).update(locked_at: time)
22
+ end
23
+
24
+ def lock_row
25
+ db_connection[table_name].where(identifier: lock.identifier).first
26
+ end
27
+
28
+ def lock_column
29
+ lock_row[:locked_at]
30
+ end
31
+
32
+ describe "#locked?" do
33
+ context "when the lock column is nulled" do
34
+ it "should return false" do
35
+ null_lock
36
+ expect(lock.locked?).to be_falsey
37
+ end
38
+ end
39
+
40
+ context "when the lock is locked" do
41
+ it "should return true" do
42
+ set_lock
43
+ expect(lock.locked?).to be_truthy
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "#unlocked" do
49
+ context "when the lock column is nulled" do
50
+ it "should return true" do
51
+ expect(lock.unlocked?).to be_truthy
52
+ end
53
+ end
54
+ end
55
+
56
+ describe "#attempt_lock" do
57
+ context "when there is no previous lock" do
58
+ it "creates the lock" do
59
+ null_lock
60
+ expect(lock.attempt_lock).to be_truthy
61
+ expect(lock.locked?).to be_truthy
62
+ end
63
+ end
64
+
65
+ context "when there is a previous lock" do
66
+ context "and it has not expired" do
67
+ it "returns false and doesn't set the lock" do
68
+ set_lock
69
+ locked_at = lock_column
70
+ expect(lock.attempt_lock).to be_falsey
71
+ locked_at_after = lock_column
72
+ expect(locked_at_after).to eq(locked_at)
73
+ end
74
+ end
75
+
76
+ context "and it has expired" do
77
+ it "returns true at sets a new lock" do
78
+ set_lock(Time.now - lock.timeout)
79
+ locked_at = lock_column
80
+ expect(lock.attempt_lock).to be_truthy
81
+ locked_at_after = lock_column
82
+ expect(locked_at_after).to_not eq(locked_at)
83
+
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "#release" do
90
+ it "releases the lock" do
91
+ set_lock
92
+ expect(lock.release).to be_truthy
93
+ expect(lock_column).to be_nil
94
+ end
95
+ end
96
+
97
+ describe "#aqcuire" do
98
+ context "when the lock is unlocked" do
99
+ it "executes the block" do
100
+ null_lock
101
+ expect { |b| lock.acquire(&b) }.to yield_control
102
+ end
103
+
104
+ it "has the lock during execution, and releases the lock afterwards" do
105
+ null_lock
106
+ lock.acquire do
107
+ expect(lock.locked?).to be_truthy
108
+ end
109
+ expect(lock_column).to be_nil
110
+ end
111
+ end
112
+
113
+ context "when the lock is locked" do
114
+ it "doesn't yield" do
115
+ set_lock
116
+ expect { |b| lock.acquire(&b) }.to_not yield_control
117
+ end
118
+ end
119
+
120
+ context "when an exception is raised" do
121
+ MyMegaException = Class.new(StandardError)
122
+
123
+ it "releases the lock" do
124
+ begin
125
+ lock.acquire do
126
+ expect(lock.locked?).to be_truthy
127
+ raise MyMegaException
128
+ end
129
+ rescue
130
+ ensure
131
+ expect(lock.locked?).to be_falsey
132
+ end
133
+ end
134
+
135
+ it "reraises the exception" do
136
+ expect { lock.acquire { raise MyMegaException } }.to raise_exception(MyMegaException)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+
3
+ module SandthornSequelProjection
4
+ describe ProcessedEventsTracker do
5
+
6
+ describe "migrated specs" do
7
+ let(:event_store) { Sandthorn.default_event_store }
8
+ let(:db_connection) { SandthornSequelProjection.configuration.db_connection }
9
+ let(:tracker) { ProcessedEventsTracker.new(identifier: :foo, event_store: event_store) }
10
+ describe "::initialize" do
11
+ it "ensures that the tracker row is present" do
12
+ tracker_row = db_connection[tracker.table_name].where(identifier: tracker.identifier).first
13
+ expect(tracker_row).to_not be_nil
14
+ expect(tracker_row[:identifier]).to eq(tracker.identifier)
15
+ expect(tracker_row[:last_processed_sequence_number]).to eq(0)
16
+ end
17
+ end
18
+
19
+ describe '#last_processed_sequence_number' do
20
+ it "returns the integer in the db" do
21
+ db_connection[tracker.table_name].
22
+ where(identifier: tracker.identifier).
23
+ update(last_processed_sequence_number: 12)
24
+ expect(tracker.last_processed_sequence_number).to eq(12)
25
+ end
26
+ end
27
+
28
+ describe "#process_events" do
29
+ let(:events) { [{sequence_number: 1}, {sequence_number: 2}] }
30
+ around do |example|
31
+ old_batch_size = SandthornSequelProjection.batch_size
32
+ SandthornSequelProjection.configuration.batch_size = 1
33
+ example.run
34
+ SandthornSequelProjection.configuration.batch_size = old_batch_size
35
+ end
36
+
37
+ before do
38
+ tracker.reset
39
+ Sandthorn.default_event_store.reset
40
+ events.each { |e| event_store.add(e) }
41
+ end
42
+
43
+ it "yields events" do
44
+ expect { |b| tracker.process_events(&b) }.to yield_successive_args([events.first], [events.last])
45
+ end
46
+
47
+ it "sets the last processed number" do
48
+ tracker.process_events { |*| }
49
+ expect(tracker.last_processed_sequence_number).to eq(2)
50
+ end
51
+
52
+ it "has the lock during the yield" do
53
+ tracker.process_events do |*|
54
+ expect(tracker.lock.locked?).to be_truthy
55
+ end
56
+ end
57
+
58
+ it "has released the lock afterwards" do
59
+ tracker.process_events { |*| }
60
+ expect(tracker.lock.locked?).to be_falsey
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ describe "non-migrated specs" do
67
+ let(:db_connection) { Sequel.sqlite }
68
+ describe "::migrate!" do
69
+ it "creates the requisite database table" do
70
+ expect { ProcessedEventsTracker.migrate!(db_connection) }.to_not raise_error
71
+ expect(db_connection.table_exists?(ProcessedEventsTracker.table_name)).to be_truthy
72
+ expected_columns = :identifier, :last_processed_sequence_number, :locked_at
73
+ expect(db_connection[ProcessedEventsTracker.table_name].columns).to include(*expected_columns)
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+
3
+ module SandthornSequelProjection
4
+
5
+ module MyModule
6
+ class TestProjection < Projection
7
+
8
+ def foo
9
+
10
+ end
11
+
12
+ def bar
13
+
14
+ end
15
+
16
+ end
17
+
18
+ class WithHandlers < TestProjection
19
+
20
+ define_event_handlers do |handlers|
21
+ handlers.define(:foo)
22
+ handlers.define(:bar)
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ describe Projection do
29
+ let(:projection) { MyModule::WithHandlers.new }
30
+
31
+ describe "::initialize" do
32
+ it "sets the handlers on the instance" do
33
+ handlers = projection.event_handlers
34
+ expect(handlers.length).to eq(2)
35
+ end
36
+ end
37
+
38
+ describe '::identifier' do
39
+ it "snake cases the class identifier" do
40
+ expect(MyModule::TestProjection.identifier).to eq("sandthorn_sequel_projection_my_module_test_projection")
41
+ end
42
+ end
43
+
44
+ describe "#update!" do
45
+
46
+ before do
47
+ event_store = Sandthorn.default_event_store
48
+ event_store.reset
49
+ event_store.add_event({sequence_number: 1})
50
+ event_store.add_event({sequence_number: 2})
51
+ end
52
+
53
+ it "fetches events and passes them on to the handlers" do
54
+ projection = MyModule::WithHandlers.new
55
+ handlers = projection.event_handlers
56
+ handlers.each do |handler|
57
+ expect(handler).to receive(:handle).twice
58
+ end
59
+ projection.update!
60
+ end
61
+ end
62
+
63
+ describe "::event_store" do
64
+ let(:klass) { Class.new(Projection) }
65
+ context "when given an event store name" do
66
+ it "sets the event store" do
67
+ klass.event_store(:foo)
68
+ expect(klass.event_store_name).to eq(:foo)
69
+ end
70
+ end
71
+
72
+ context "when given no argument" do
73
+ context "when an event store has been configured" do
74
+ before do
75
+ klass.event_store(:foo)
76
+ end
77
+ it "fetches the event store" do
78
+ expect(SandthornSequelProjection).to receive(:find_event_store).with(:foo)
79
+ klass.event_store
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "::define_event_handlers" do
86
+ it "yields an EventHandlerCollection" do
87
+ expect { |b| MyModule::TestProjection.define_event_handlers(&b) }.to yield_with_args(EventHandlerCollection)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,32 @@
1
+ module SandthornSequelProjection
2
+ describe Runner do
3
+
4
+ module FakeProjection
5
+ def initialize(*); end
6
+ def migrate!; end
7
+ def update!; end
8
+ end
9
+
10
+ class Projection1; include FakeProjection; end
11
+ class Projection2; include FakeProjection; end
12
+
13
+ let(:manifest) { Manifest.new(Projection1, Projection2) }
14
+
15
+ let(:runner) { SandthornSequelProjection::Runner.new(manifest) }
16
+
17
+ describe "#run" do
18
+ it "migrates all projections" do
19
+ expect_any_instance_of(Projection1).to receive(:migrate!).once
20
+ expect_any_instance_of(Projection2).to receive(:migrate!).once
21
+ runner.run(false)
22
+ end
23
+
24
+ it "updates all projections" do
25
+ expect_any_instance_of(Projection1).to receive(:update!).once
26
+ expect_any_instance_of(Projection2).to receive(:update!).once
27
+ runner.run(false)
28
+ end
29
+ end
30
+
31
+ end
32
+ end