event_store_client 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/MIT-LICENSE +20 -0
- data/README.md +148 -0
- data/Rakefile +19 -0
- data/lib/event_store_client.rb +23 -0
- data/lib/event_store_client/broker.rb +25 -0
- data/lib/event_store_client/client.rb +90 -0
- data/lib/event_store_client/configuration.rb +28 -0
- data/lib/event_store_client/connection.rb +76 -0
- data/lib/event_store_client/endpoint.rb +16 -0
- data/lib/event_store_client/event.rb +25 -0
- data/lib/event_store_client/mapper.rb +8 -0
- data/lib/event_store_client/mapper/default.rb +33 -0
- data/lib/event_store_client/serializer/json.rb +13 -0
- data/lib/event_store_client/store_adapter.rb +10 -0
- data/lib/event_store_client/store_adapter/api/client.rb +96 -0
- data/lib/event_store_client/store_adapter/api/connection.rb +36 -0
- data/lib/event_store_client/store_adapter/api/request_method.rb +30 -0
- data/lib/event_store_client/store_adapter/in_memory.rb +77 -0
- data/lib/event_store_client/subscription.rb +15 -0
- data/lib/event_store_client/subscriptions.rb +38 -0
- data/lib/event_store_client/types.rb +9 -0
- data/lib/event_store_client/version.rb +5 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d3deeba909c8bf46f05e7994ae5ebef841e622c17032f0259387871335f80e98
|
|
4
|
+
data.tar.gz: 0bc3e13528fa696506b3d34ca2b00004a736f30bb62d379f41ef50e4a2b180ab
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2606efc97b2695dc52d358a9c0bc967d0583c2b58b95b27289c6f9e4dbac04a87bb134dfc8a1e0b251ca11b08c5f0c28841c25f48fdbd4a33db2dd37a0369727
|
|
7
|
+
data.tar.gz: f1b690148757d9f316127b272c03d70558fc53a8868dbf0058d7669bcdd702b92849d44002dd6eb2367982525fdf25299931d8c8563c5c7ed7d3527f24d9ddc3
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2019 Sebastian Wilgosz
|
|
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,148 @@
|
|
|
1
|
+
# EventStoreClient
|
|
2
|
+
|
|
3
|
+
An easy-to use API client for connecting ruby applications with https://eventstore.org/
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
Add this line to your application's Gemfile:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
gem 'event_store_client'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
And then execute:
|
|
13
|
+
```bash
|
|
14
|
+
$ bundle
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
```bash
|
|
19
|
+
$ gem install event_store_client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### EventStore engine setup
|
|
25
|
+
|
|
26
|
+
1. Download Event Store From https://eventstore.org/downloads/ or docker
|
|
27
|
+
|
|
28
|
+
` docker pull eventstore/eventstore`
|
|
29
|
+
|
|
30
|
+
2. Run the Event Store server
|
|
31
|
+
|
|
32
|
+
`docker run --name eventstore -it -p 2113:2113 -p 1113:1113 eventstore/eventstore`
|
|
33
|
+
|
|
34
|
+
3. Set Basic HTTP auth enviornment variables #below are defaults
|
|
35
|
+
- export EVENT_STORE_USER=admin
|
|
36
|
+
- export EVENT_STORE_PASSWORD=changeit
|
|
37
|
+
|
|
38
|
+
Ref: https://eventstore.org/docs/http-api/security
|
|
39
|
+
|
|
40
|
+
4. Login to admin panel http://localhost:2113 and enable Projections for Event-Types
|
|
41
|
+
|
|
42
|
+
### Configure EventStoreClient
|
|
43
|
+
|
|
44
|
+
Before you start, add this to the `initializer` or to the top of your script:
|
|
45
|
+
|
|
46
|
+
`EventStoreClient.configure`
|
|
47
|
+
|
|
48
|
+
### Create Dummy event and dummy Handler
|
|
49
|
+
|
|
50
|
+
To test out the behavior, you'll need a sample event and handler to work with:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Sample Event using dry-struct (recommended)
|
|
54
|
+
require 'dry-struct'
|
|
55
|
+
class SomethingHappened < Dry::Struct
|
|
56
|
+
attribute :data, EventStoreClient::Types::Strict::Hash
|
|
57
|
+
attribute :metadata, EventStoreClient::Types::Strict::Hash
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Sample Event without types check (not recommended)
|
|
61
|
+
|
|
62
|
+
class SomethingHappened < Dry::Struct
|
|
63
|
+
attr_reader :data, :metadata
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def initialize(data: {}, metadata: {})
|
|
68
|
+
@data = data
|
|
69
|
+
@metadata = metadata
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
event = SomethingHappened.new(
|
|
74
|
+
data: { user_id: SecureRandom.uuid, title: "Something happened" },
|
|
75
|
+
metadata: {}
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Now create a handler. It can be anything, which responds to a `call` method
|
|
80
|
+
with an event being passed as an argument.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class DummyHandler
|
|
84
|
+
def self.call(event)
|
|
85
|
+
puts "Handled #{event.class.name}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# initialize the client
|
|
93
|
+
client = EventStoreClient::Client.new
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Publishing events
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
client.publish(stream: 'newstream', events: [event])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Reading from a stream
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
events = client.read('newstream')
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Changing reading direction
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
events = client.read('newstream', direction: 'backward') #default 'forward'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Subscribing to events
|
|
115
|
+
|
|
116
|
+
# Using automatic pooling
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
client.subscribe(DummyHandler, to: [SomethingHappened])
|
|
120
|
+
|
|
121
|
+
# now try to publish several events
|
|
122
|
+
10.times { client.publish(stream: 'newstream', events: [event]) }
|
|
123
|
+
|
|
124
|
+
You can also publish multiple events at once
|
|
125
|
+
|
|
126
|
+
events = (1..10).map { event }
|
|
127
|
+
client.publish(stream: 'newstream', events: events)
|
|
128
|
+
|
|
129
|
+
# .... wait a little bit ... Your handler should be called for every single event you publish
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Stop pooling
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
client.stop_pooling
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
Do you want to contribute? Welcome!
|
|
141
|
+
|
|
142
|
+
1. Fork repository
|
|
143
|
+
2. Create Issue
|
|
144
|
+
3. Create PR ;)
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require 'rdoc/task'
|
|
10
|
+
|
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
13
|
+
rdoc.title = 'EventStoreClient'
|
|
14
|
+
rdoc.options << '--line-numbers'
|
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require 'bundler/gem_tasks'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
require 'event_store_client/configuration'
|
|
7
|
+
require 'event_store_client/types'
|
|
8
|
+
require 'event_store_client/event'
|
|
9
|
+
|
|
10
|
+
require 'event_store_client/serializer/json'
|
|
11
|
+
|
|
12
|
+
require 'event_store_client/mapper'
|
|
13
|
+
|
|
14
|
+
require 'event_store_client/endpoint'
|
|
15
|
+
|
|
16
|
+
require 'event_store_client/store_adapter'
|
|
17
|
+
|
|
18
|
+
require 'event_store_client/connection'
|
|
19
|
+
|
|
20
|
+
require 'event_store_client/subscription'
|
|
21
|
+
require 'event_store_client/subscriptions'
|
|
22
|
+
require 'event_store_client/broker'
|
|
23
|
+
require 'event_store_client/client'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
class Broker
|
|
5
|
+
def call(subscriptions)
|
|
6
|
+
subscriptions.each do |subscription|
|
|
7
|
+
new_events = connection.consume_feed(subscription.stream, subscription.name)
|
|
8
|
+
next if new_events.none?
|
|
9
|
+
new_events.each do |event|
|
|
10
|
+
subscription.subscribers.each do |subscriber|
|
|
11
|
+
subscriber.call(event)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :connection
|
|
20
|
+
|
|
21
|
+
def initialize(connection:)
|
|
22
|
+
@connection = connection
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
|
|
5
|
+
module EventStoreClient
|
|
6
|
+
class Client
|
|
7
|
+
NoCallMethodOnSubscriber = Class.new(StandardError)
|
|
8
|
+
|
|
9
|
+
def publish(stream:, events:, expected_version: nil)
|
|
10
|
+
connection.publish(stream: stream, events: events, expected_version: expected_version)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(stream, direction: 'forward')
|
|
14
|
+
connection.read(stream, direction: direction)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def subscribe(subscriber, to: [], pooling: true)
|
|
18
|
+
raise NoCallMethodOnSubscriber unless subscriber.respond_to?(:call)
|
|
19
|
+
@subscriptions.create(subscriber, to)
|
|
20
|
+
pool if pooling
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pool(interval: 5)
|
|
24
|
+
return if @pooling_started
|
|
25
|
+
@pooling_started = true
|
|
26
|
+
thread1 = Thread.new do
|
|
27
|
+
loop do
|
|
28
|
+
create_pid_file
|
|
29
|
+
Thread.handle_interrupt(Interrupt => :never) {
|
|
30
|
+
begin
|
|
31
|
+
Thread.handle_interrupt(Interrupt => :immediate) {
|
|
32
|
+
broker.call(subscriptions)
|
|
33
|
+
}
|
|
34
|
+
rescue Exception => e
|
|
35
|
+
# When the thread had been interrupted or broker.call returned an error
|
|
36
|
+
sleep(interval) # wait for events to be processed
|
|
37
|
+
delete_pid_file
|
|
38
|
+
error_handler.call(e) if error_handler
|
|
39
|
+
ensure
|
|
40
|
+
# this code is run always
|
|
41
|
+
Thread.stop
|
|
42
|
+
end
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
thread2 = Thread.new do
|
|
47
|
+
loop { sleep 1; break unless thread1.alive?; thread1.run }
|
|
48
|
+
end
|
|
49
|
+
@threads = [thread1, thread2]
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stop_pooling
|
|
54
|
+
return if @threads.none?
|
|
55
|
+
@threads.each do |thread|
|
|
56
|
+
thread.kill
|
|
57
|
+
end
|
|
58
|
+
@pooling_started = false
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_accessor :connection, :service_name
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
attr_reader :subscriptions, :broker, :error_handler
|
|
67
|
+
|
|
68
|
+
def config
|
|
69
|
+
EventStoreClient::Configuration.instance
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def initialize
|
|
73
|
+
@threads = []
|
|
74
|
+
@connection ||= Connection.new
|
|
75
|
+
@error_handler ||= config.error_handler
|
|
76
|
+
@service_name ||= 'default'
|
|
77
|
+
@broker ||= Broker.new(connection: connection)
|
|
78
|
+
@subscriptions ||= Subscriptions.new(connection: connection, service: config.service_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def create_pid_file
|
|
82
|
+
return unless File.exists?(config.pid_path)
|
|
83
|
+
File.open(config.pid_path, 'w') { |file| file.write(SecureRandom.uuid) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def delete_pid_file
|
|
87
|
+
File.delete(config.pid_path)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'singleton'
|
|
5
|
+
|
|
6
|
+
module EventStoreClient
|
|
7
|
+
class Configuration
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
attr_accessor :host, :port, :per_page, :service_name, :mapper, :error_handler, :pid_path
|
|
11
|
+
|
|
12
|
+
def configure
|
|
13
|
+
yield(self) if block_given?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@host = 'http://localhost'
|
|
20
|
+
@port = 2113
|
|
21
|
+
@per_page = 20
|
|
22
|
+
@pid_path = 'tmp/pool.pid'
|
|
23
|
+
@mapper = Mapper::Default.new
|
|
24
|
+
@service_name = 'default'
|
|
25
|
+
@error_handler = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
class Connection
|
|
5
|
+
def publish(stream:, events:, expected_version: nil)
|
|
6
|
+
serialized_events = events.map { |event| mapper.serialize(event) }
|
|
7
|
+
client.append_to_stream(
|
|
8
|
+
stream, serialized_events, expected_version: expected_version
|
|
9
|
+
)
|
|
10
|
+
serialized_events
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(stream, direction: 'forward')
|
|
14
|
+
response =
|
|
15
|
+
client.read(stream, start: 0, direction: direction)
|
|
16
|
+
return [] unless response.body&.present?
|
|
17
|
+
JSON.parse(response.body)['entries'].map do |entry|
|
|
18
|
+
event = EventStoreClient::Event.new(
|
|
19
|
+
id: entry['eventId'],
|
|
20
|
+
type: entry['eventType'],
|
|
21
|
+
data: entry['data'],
|
|
22
|
+
metadata: entry['metaData']
|
|
23
|
+
)
|
|
24
|
+
mapper.deserialize(event)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete_stream(stream); end
|
|
29
|
+
|
|
30
|
+
def subscribe(stream, name:)
|
|
31
|
+
client.subscribe_to_stream(stream, name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def consume_feed(stream, subscription)
|
|
35
|
+
response = client.consume_feed(stream, subscription)
|
|
36
|
+
return [] unless response.body
|
|
37
|
+
body = JSON.parse(response.body)
|
|
38
|
+
ack_uri =
|
|
39
|
+
body['links'].find { |link| link['relation'] == 'ackAll' }.
|
|
40
|
+
try(:[], 'uri')
|
|
41
|
+
events = body['entries'].map do |entry|
|
|
42
|
+
event = EventStoreClient::Event.new(
|
|
43
|
+
id: entry['eventId'],
|
|
44
|
+
type: entry['eventType'],
|
|
45
|
+
data: entry['data'] || '{}',
|
|
46
|
+
metadata: entry['isMetaData'] ? entry['metaData'] : '{}'
|
|
47
|
+
)
|
|
48
|
+
mapper.deserialize(event)
|
|
49
|
+
end
|
|
50
|
+
client.ack(ack_uri)
|
|
51
|
+
events
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :host, :port, :mapper, :per_page
|
|
57
|
+
|
|
58
|
+
def config
|
|
59
|
+
EventStoreClient::Configuration.instance
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize
|
|
63
|
+
@host = config.host
|
|
64
|
+
@port = config.port
|
|
65
|
+
@per_page = config.per_page
|
|
66
|
+
@mapper = config.mapper
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def client
|
|
70
|
+
@client ||=
|
|
71
|
+
EventStoreClient::StoreAdapter::Api::Client.new(
|
|
72
|
+
host: host, port: port, per_page: per_page
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
|
|
5
|
+
module EventStoreClient
|
|
6
|
+
class Endpoint < Dry::Struct
|
|
7
|
+
def url
|
|
8
|
+
"#{host}:#{port}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
attribute :host, Types::String
|
|
14
|
+
attribute :port, Types::Coercible::Integer
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-struct'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module EventStoreClient
|
|
8
|
+
class Event < Dry::Struct
|
|
9
|
+
attr_reader :id
|
|
10
|
+
|
|
11
|
+
attribute :data, Types::Strict::String.default('{}')
|
|
12
|
+
attribute :metadata, Types::Strict::String.default('{}')
|
|
13
|
+
attribute :type, Types::Strict::String
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def initialize(**args)
|
|
18
|
+
@id = SecureRandom.uuid
|
|
19
|
+
hash_meta =
|
|
20
|
+
JSON.parse(args[:metadata] || '{}').merge(created_at: Time.now)
|
|
21
|
+
args[:metadata] = JSON.generate(hash_meta)
|
|
22
|
+
super(args)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
module Mapper
|
|
5
|
+
class Default
|
|
6
|
+
def serialize(event)
|
|
7
|
+
Event.new(
|
|
8
|
+
metadata: serializer.serialize(event.metadata),
|
|
9
|
+
data: serializer.serialize(event.data),
|
|
10
|
+
type: event.class.to_s
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def deserialize(event)
|
|
15
|
+
metadata = serializer.deserialize(event.metadata)
|
|
16
|
+
data = serializer.deserialize(event.data)
|
|
17
|
+
|
|
18
|
+
Object.const_get(event.type).new(
|
|
19
|
+
metadata: metadata,
|
|
20
|
+
data: data
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :serializer
|
|
27
|
+
|
|
28
|
+
def initialize(serializer: Serializer::Json)
|
|
29
|
+
@serializer = serializer
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
module StoreAdapter
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require 'event_store_client/store_adapter/api/request_method'
|
|
9
|
+
require 'event_store_client/store_adapter/api/connection'
|
|
10
|
+
require 'event_store_client/store_adapter/api/client'
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
module StoreAdapter
|
|
5
|
+
module Api
|
|
6
|
+
class Client
|
|
7
|
+
def append_to_stream(stream_name, events, expected_version: nil)
|
|
8
|
+
headers = {
|
|
9
|
+
'ES-ExpectedVersion' => expected_version.to_s
|
|
10
|
+
}.reject { |_key, val| val.empty? }
|
|
11
|
+
|
|
12
|
+
data = [events].flatten.map do |event|
|
|
13
|
+
{
|
|
14
|
+
eventId: event.id,
|
|
15
|
+
eventType: event.type,
|
|
16
|
+
data: event.data,
|
|
17
|
+
metadata: event.metadata
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
make_request(:post, "/streams/#{stream_name}", body: data, headers: headers)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete_stream(stream_name, hard_delete)
|
|
25
|
+
headers = JSON_HEADERS.merge('ES-HardDelete' => hard_delete.to_s)
|
|
26
|
+
make_request(:delete, "/streams/#{stream_name}", {}, headers)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read(stream_name, direction: 'forward', start: 0, count: per_page)
|
|
30
|
+
make_request(:get, "/streams/#{stream_name}/#{start}/#{direction}/#{count}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subscribe_to_stream(
|
|
34
|
+
stream_name, subscription_name, stats: true, start_from: 0, retries: 5
|
|
35
|
+
)
|
|
36
|
+
make_request(
|
|
37
|
+
:put,
|
|
38
|
+
"/subscriptions/#{stream_name}/#{subscription_name}",
|
|
39
|
+
body: {
|
|
40
|
+
extraStatistics: stats,
|
|
41
|
+
startFrom: start_from,
|
|
42
|
+
maxRetryCount: retries,
|
|
43
|
+
resolveLinkTos: true
|
|
44
|
+
},
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type" => "application/json"
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def consume_feed(
|
|
52
|
+
stream_name,
|
|
53
|
+
subscription_name,
|
|
54
|
+
start: 0,
|
|
55
|
+
count: 1,
|
|
56
|
+
long_pool: 0
|
|
57
|
+
)
|
|
58
|
+
headers = long_pool > 0 ? { "ES-LongPoll" => "#{long_pool}" } : {}
|
|
59
|
+
headers['Content-Type'] = 'application/vnd.eventstore.competingatom+json'
|
|
60
|
+
headers['Accept'] = 'application/vnd.eventstore.competingatom+json'
|
|
61
|
+
make_request(
|
|
62
|
+
:get,
|
|
63
|
+
"/subscriptions/#{stream_name}/#{subscription_name}/#{count}",
|
|
64
|
+
headers: headers
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ack(url)
|
|
69
|
+
make_request(:post, url)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :endpoint, :per_page
|
|
75
|
+
|
|
76
|
+
def initialize(host:, port:, per_page: 20)
|
|
77
|
+
@endpoint = Endpoint.new(host: host, port: port)
|
|
78
|
+
@per_page = per_page
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def make_request(method_name, path, body: {}, headers: {})
|
|
82
|
+
method = RequestMethod.new(method_name)
|
|
83
|
+
connection.send(method.to_s, path) do |req|
|
|
84
|
+
req.headers = req.headers.merge(headers)
|
|
85
|
+
req.body = body.to_json
|
|
86
|
+
req.params['embed'] = 'body' if method == :get
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def connection
|
|
91
|
+
@connection ||= Api::Connection.new(endpoint).call
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module EventStoreClient
|
|
6
|
+
module StoreAdapter
|
|
7
|
+
module Api
|
|
8
|
+
class Connection
|
|
9
|
+
def call
|
|
10
|
+
Faraday.new(
|
|
11
|
+
url: endpoint.url,
|
|
12
|
+
headers: DEFAULT_HEADERS
|
|
13
|
+
) do |conn|
|
|
14
|
+
conn.basic_auth(ENV['EVENT_STORE_USER'], ENV['EVENT_STORE_PASSWORD'])
|
|
15
|
+
conn.adapter Faraday.default_adapter
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def initialize(endpoint)
|
|
22
|
+
@endpoint = endpoint
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :endpoint
|
|
26
|
+
|
|
27
|
+
DEFAULT_HEADERS = {
|
|
28
|
+
'Content-Type' => 'application/vnd.eventstore.events+json'
|
|
29
|
+
# 'Accept' => 'application/vnd.eventstore.atom+json',
|
|
30
|
+
# 'ES-EventType' => 'UserRegistered',
|
|
31
|
+
# 'ES-EventId' => SecureRandom.uuid
|
|
32
|
+
}.freeze
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
module StoreAdapter
|
|
5
|
+
module Api
|
|
6
|
+
class RequestMethod
|
|
7
|
+
InvalidMethodError = Class.new(StandardError)
|
|
8
|
+
def ==(other)
|
|
9
|
+
name == other.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :name
|
|
19
|
+
|
|
20
|
+
def initialize(name)
|
|
21
|
+
raise InvalidMethodError unless name.to_s.in?(SUPPORTED_METHODS)
|
|
22
|
+
|
|
23
|
+
@name = name.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
SUPPORTED_METHODS = %w[get post put].freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
module StoreAdapter
|
|
5
|
+
class InMemory
|
|
6
|
+
attr_reader :event_store
|
|
7
|
+
|
|
8
|
+
def append_to_stream(stream_name, events, expected_version: nil)
|
|
9
|
+
event_store[stream_name] = [] unless event_store.key?(stream_name)
|
|
10
|
+
|
|
11
|
+
[events].flatten.each do |event|
|
|
12
|
+
event_store[stream_name].unshift(
|
|
13
|
+
'eventId' => event.id,
|
|
14
|
+
'data' => event.data,
|
|
15
|
+
'eventType' => event.type,
|
|
16
|
+
'metadata' => event.metadata,
|
|
17
|
+
'positionEventNumber' => event_store[stream_name].length
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete_stream(stream_name, hard_delete: false)
|
|
23
|
+
event_store.delete(stream_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def read_stream_backward(stream_name, start: 0, count: per_page)
|
|
27
|
+
return [] unless event_store.key?(stream_name)
|
|
28
|
+
|
|
29
|
+
start = start == 0 ? event_store[stream_name].length - 1 : start
|
|
30
|
+
last_index = start - count
|
|
31
|
+
entries = event_store[stream_name].select do |event|
|
|
32
|
+
event['positionEventNumber'] > last_index &&
|
|
33
|
+
event['positionEventNumber'] <= start
|
|
34
|
+
end
|
|
35
|
+
{
|
|
36
|
+
'entries' => entries,
|
|
37
|
+
'links' => links(stream_name, last_index, 'next', entries, count)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def read_stream_forward(stream_name, start: 0, count: per_page)
|
|
42
|
+
return [] unless event_store.key?(stream_name)
|
|
43
|
+
|
|
44
|
+
last_index = start + count
|
|
45
|
+
entries = event_store[stream_name].reverse.select do |event|
|
|
46
|
+
event['positionEventNumber'] < last_index &&
|
|
47
|
+
event['positionEventNumber'] >= start
|
|
48
|
+
end
|
|
49
|
+
{
|
|
50
|
+
'entries' => entries,
|
|
51
|
+
'links' => links(stream_name, last_index, 'previous', entries, count)
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
attr_reader :endpoint, :per_page
|
|
58
|
+
|
|
59
|
+
def initialize(host:, port:, per_page: 20)
|
|
60
|
+
@endpoint = Endpoint.new(host: host, port: port)
|
|
61
|
+
@per_page = per_page
|
|
62
|
+
@event_store = {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def links(stream_name, batch_size, direction, entries, count)
|
|
66
|
+
if entries.empty? || batch_size < 0
|
|
67
|
+
[]
|
|
68
|
+
else
|
|
69
|
+
[{
|
|
70
|
+
'uri' => "http://#{endpoint.url}/streams/#{stream_name}/#{batch_size}/#{direction}/#{count}",
|
|
71
|
+
'relation' => direction
|
|
72
|
+
}]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
class Subscription
|
|
5
|
+
attr_accessor :subscribers
|
|
6
|
+
attr_reader :stream, :name
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
def initialize(type:, name:)
|
|
10
|
+
@name = name
|
|
11
|
+
@subscribers = []
|
|
12
|
+
@stream = "$et-#{type}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventStoreClient
|
|
4
|
+
class Subscriptions
|
|
5
|
+
def create(subscriber, event_types)
|
|
6
|
+
event_types.each do |type|
|
|
7
|
+
subscription = subscriptions[type.to_s] || Subscription.new(type: type, name: service)
|
|
8
|
+
subscription.subscribers |= [subscriber]
|
|
9
|
+
create_subscription(subscription) unless @subscriptions.key?(type.to_s)
|
|
10
|
+
@subscriptions[type.to_s] = subscription
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def each
|
|
15
|
+
subscriptions.values.each do |subscription|
|
|
16
|
+
yield(subscription)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_updates(subscription)
|
|
21
|
+
connection.consume_feed(subscription.stream, subscription.name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def create_subscription(subscription)
|
|
27
|
+
connection.subscribe(subscription.stream, name: subscription.name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :connection, :subscriptions, :service
|
|
31
|
+
|
|
32
|
+
def initialize(connection:, service: 'default')
|
|
33
|
+
@connection = connection
|
|
34
|
+
@service = service
|
|
35
|
+
@subscriptions = {}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: event_store_client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sebastian Wilgosz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-12-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dry-struct
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.1.1
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 1.1.1
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.17.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.17.0
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rss
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 0.2.8
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 0.2.8
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
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
|
+
description: Easy to use client for event-sources applications written in ruby
|
|
70
|
+
email:
|
|
71
|
+
- sebastian@driggl.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- MIT-LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- Rakefile
|
|
79
|
+
- lib/event_store_client.rb
|
|
80
|
+
- lib/event_store_client/broker.rb
|
|
81
|
+
- lib/event_store_client/client.rb
|
|
82
|
+
- lib/event_store_client/configuration.rb
|
|
83
|
+
- lib/event_store_client/connection.rb
|
|
84
|
+
- lib/event_store_client/endpoint.rb
|
|
85
|
+
- lib/event_store_client/event.rb
|
|
86
|
+
- lib/event_store_client/mapper.rb
|
|
87
|
+
- lib/event_store_client/mapper/default.rb
|
|
88
|
+
- lib/event_store_client/serializer/json.rb
|
|
89
|
+
- lib/event_store_client/store_adapter.rb
|
|
90
|
+
- lib/event_store_client/store_adapter/api/client.rb
|
|
91
|
+
- lib/event_store_client/store_adapter/api/connection.rb
|
|
92
|
+
- lib/event_store_client/store_adapter/api/request_method.rb
|
|
93
|
+
- lib/event_store_client/store_adapter/in_memory.rb
|
|
94
|
+
- lib/event_store_client/subscription.rb
|
|
95
|
+
- lib/event_store_client/subscriptions.rb
|
|
96
|
+
- lib/event_store_client/types.rb
|
|
97
|
+
- lib/event_store_client/version.rb
|
|
98
|
+
homepage: https://github.com/yousty/event_store_client
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
allowed_push_host: https://rubygems.org
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.0.4
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Ruby integration for https://eventstore.org
|
|
122
|
+
test_files: []
|