pg_eventstore 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/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/db/extensions.sql +2 -0
- data/db/indexes.sql +13 -0
- data/db/primary_and_foreign_keys.sql +11 -0
- data/db/tables.sql +21 -0
- data/docs/appending_events.md +170 -0
- data/docs/configuration.md +82 -0
- data/docs/events_and_streams.md +45 -0
- data/docs/multiple_commands.md +46 -0
- data/docs/reading_events.md +161 -0
- data/docs/writing_middleware.md +160 -0
- data/lib/pg_eventstore/abstract_command.rb +18 -0
- data/lib/pg_eventstore/client.rb +133 -0
- data/lib/pg_eventstore/commands/append.rb +61 -0
- data/lib/pg_eventstore/commands/multiple.rb +14 -0
- data/lib/pg_eventstore/commands/read.rb +24 -0
- data/lib/pg_eventstore/commands.rb +6 -0
- data/lib/pg_eventstore/config.rb +30 -0
- data/lib/pg_eventstore/connection.rb +97 -0
- data/lib/pg_eventstore/errors.rb +107 -0
- data/lib/pg_eventstore/event.rb +59 -0
- data/lib/pg_eventstore/event_class_resolver.rb +17 -0
- data/lib/pg_eventstore/event_serializer.rb +27 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +103 -0
- data/lib/pg_eventstore/middleware.rb +15 -0
- data/lib/pg_eventstore/pg_result_deserializer.rb +48 -0
- data/lib/pg_eventstore/queries.rb +127 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +187 -0
- data/lib/pg_eventstore/rspec/has_option_matcher.rb +90 -0
- data/lib/pg_eventstore/sql_builder.rb +126 -0
- data/lib/pg_eventstore/stream.rb +72 -0
- data/lib/pg_eventstore/tasks/setup.rake +37 -0
- data/lib/pg_eventstore/version.rb +5 -0
- data/lib/pg_eventstore.rb +85 -0
- data/pg_eventstore.gemspec +40 -0
- metadata +113 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
# Reading Events
|
2
|
+
|
3
|
+
## Reading from a specific stream
|
4
|
+
|
5
|
+
The easiest way to read a stream forwards is to supply a `PgEventstore::Stream` object.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
9
|
+
PgEventstore.client.read(stream)
|
10
|
+
# => [#<PgEventstore::Event 0x1>, #<PgEventstore::Event 0x1>, ...]
|
11
|
+
```
|
12
|
+
|
13
|
+
### max_count
|
14
|
+
|
15
|
+
You can provide the `:max_count` option. This option determines how many records to return in a response. Default is `1000` and it can be changed with the `:max_count` configuration setting (see [**"Configuration"**](configuration.md) chapter):
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
19
|
+
PgEventstore.client.read(stream, options: { max_count: 100 })
|
20
|
+
```
|
21
|
+
|
22
|
+
### resolve_link_tos
|
23
|
+
|
24
|
+
When reading streams with projected events (links to other events) you can chose to resolve those links by setting `resolve_link_tos` to `true`, returning the original event instead of the "link" event.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
28
|
+
PgEventstore.client.read(stream, options: { resolve_link_tos: true })
|
29
|
+
```
|
30
|
+
|
31
|
+
### from_revision
|
32
|
+
|
33
|
+
You can define from which revision number you would like to start to read events:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
37
|
+
PgEventstore.client.read(stream, options: { from_revision: 2 })
|
38
|
+
```
|
39
|
+
|
40
|
+
### direction
|
41
|
+
|
42
|
+
As well as being able to read a stream forwards you can also go backwards. This can be achieved by providing the `:direction` option:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
46
|
+
PgEventstore.client.read(stream, options: { direction: 'Backwards' })
|
47
|
+
```
|
48
|
+
|
49
|
+
## Checking if stream exists
|
50
|
+
|
51
|
+
In case a stream with given name does not exist, a `PgEventstore::StreamNotFoundError` error will be raised:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
begin
|
55
|
+
stream = PgEventstore::Stream.new(context: 'non-existing-context', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
56
|
+
PgEventstore.client.read(stream)
|
57
|
+
rescue PgEventstore::StreamNotFoundError => e
|
58
|
+
puts e.message # => Stream #<PgEventstore::Stream:0x01> does not exist.
|
59
|
+
puts e.stream # => #<PgEventstore::Stream:0x01>
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Reading from the "all" stream
|
64
|
+
|
65
|
+
"all" stream definition means that you don't scope your events when reading them from the database. To get the "all" `PgEventstore::Stream` instance you have to call the `all_stream` method:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
PgEventstore::Stream.all_stream
|
69
|
+
```
|
70
|
+
|
71
|
+
Now you can use it to read from the "all" stream:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream)
|
75
|
+
```
|
76
|
+
|
77
|
+
You can read from a specific position of the "all" stream. This is very similar to reading from a specific revision of a specific stream, but instead of the `:from_revision` option you have to provide the `:from_position` option:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { from_position: 9023, direction: 'Backwards' })
|
81
|
+
```
|
82
|
+
|
83
|
+
## Middlewares
|
84
|
+
|
85
|
+
If you would like to skip some of your registered middlewares from processing events after they being read from a stream - you should use the `:middlewares` argument which allows you to override the list of middlewares you would like to use.
|
86
|
+
|
87
|
+
Let's say you have these registered middlewares:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
PgEventstore.configure do |config|
|
91
|
+
config.middlewares = { foo: FooMiddleware.new, bar: BarMiddleware.new, baz: BazMiddleware.new }
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
And you want to skip `FooMiddleware` and `BazMiddleware`. You simply have to provide an array of corresponding middleware keys you would like to use:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, middlewares: %i[bar])
|
99
|
+
```
|
100
|
+
|
101
|
+
See [Writing middleware](writing_middleware.md) chapter for info about what is middleware and how to implement it.
|
102
|
+
|
103
|
+
## Filtering
|
104
|
+
|
105
|
+
When reading events, you can additionally filter the result. Available attributes for filtering depend on the type of stream you are reading from. Reading from the "all" stream supports filters by stream attributes and event types. Reading from a specific stream supports filters by event types only.
|
106
|
+
|
107
|
+
### Specific stream filtering
|
108
|
+
|
109
|
+
Filtering events by their types:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
stream = PgEventstore::Stream.new(context: 'MYAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
113
|
+
PgEventstore.client.read(stream, options: { filter: { event_types: %w[Foo Bar] } })
|
114
|
+
```
|
115
|
+
|
116
|
+
### "all" stream filtering
|
117
|
+
|
118
|
+
**Warning** There is a restriction on a set of stream attributes that can be used when filtering an "all" stream result. Available combinations:
|
119
|
+
|
120
|
+
- `:context`
|
121
|
+
- `:context` and `:stream_name`
|
122
|
+
- `:context`, `:stream_name` and `:stream_id`
|
123
|
+
|
124
|
+
All other combinations, like providing only `:stream_name` or providing `:context` with `:stream_id` will be ignored.
|
125
|
+
|
126
|
+
|
127
|
+
Filtering events by type:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { event_types: %w[Foo Bar] } })
|
131
|
+
```
|
132
|
+
|
133
|
+
Filtering events by context:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext' }] } })
|
137
|
+
```
|
138
|
+
|
139
|
+
Filtering events by context and name:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext', stream_name: 'User' }] } })
|
143
|
+
```
|
144
|
+
|
145
|
+
Filtering events by stream context, stream name and stream id:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae' }] } })
|
149
|
+
```
|
150
|
+
|
151
|
+
You can provide several sets of stream's attributes. The result will be a union of events that match those criteria. For example, next query will return all events that belong to streams with `AnotherContext` context and all events that belong to streams with `MyAwesomeContext` context and `User` stream name:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'AnotherContext' }, { context: 'MyAwesomeContext', stream_name: 'User' }] } })
|
155
|
+
```
|
156
|
+
|
157
|
+
You can also mix filtering by stream's attributes and event types. The result will be intersection of events matching stream's attributes and event's types. For example, next query will return events which type is either `Foo` or `Bar` and which belong to a stream with `MyAwesomeContext` context:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
|
161
|
+
```
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# Writing middleware
|
2
|
+
|
3
|
+
Middlewares are objects that modify events before they are appended to a stream, or right after they are read from a stream. Middleware object must respond to `#serialize` and `#deserialize` methods. The `#serialize` method is called each time an event is going to be appended. The `#deserialize` method is called each time an event is read from a stream. There are two ways how you can define a middleware:
|
4
|
+
|
5
|
+
- by defining your class and including `PgEventstore::Middleware` module in it. This way you can override only one of its methods, or both of them. Example:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# Override #serialize only to define your custom logic
|
9
|
+
class MyAwesomeSerializer
|
10
|
+
include PgEventstore::Middleware
|
11
|
+
|
12
|
+
def serialize(event)
|
13
|
+
do_something
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Override #deserialize only to define your custom logic
|
18
|
+
class MyAwesomeDeserializer
|
19
|
+
include PgEventstore::Middleware
|
20
|
+
|
21
|
+
def deserialize(event)
|
22
|
+
do_something
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Override both #serialize and #deserialize methods
|
27
|
+
class MyAwesomeMiddleware
|
28
|
+
include PgEventstore::Middleware
|
29
|
+
|
30
|
+
def serialize(event)
|
31
|
+
do_something
|
32
|
+
end
|
33
|
+
|
34
|
+
def deserialize(event)
|
35
|
+
do_something
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Configure your middlewares
|
40
|
+
PgEventstore.configure do |config|
|
41
|
+
config.middlewares = { my_awesome_serializer: MyAwesomeSerializer.new, my_awesome_deserializer: MyAwesomeDeserializer.new, my_awesome_middleware: MyAwesomeMiddleware.new }
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
- implement your own object that implements `#serialize` and `#deserialize` methods. Example:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'securerandom'
|
49
|
+
|
50
|
+
# Provide some basic functionality for large payload extraction implementations. Every event in your app will be inherited from this class.
|
51
|
+
class MyAppAbstractEvent < PgEventstore::Event
|
52
|
+
def self.payload_store_fields
|
53
|
+
[]
|
54
|
+
end
|
55
|
+
|
56
|
+
def fields_with_large_payloads
|
57
|
+
data.slice(*self.class.payload_store_fields)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class DescriptionChangedEvent < MyAppAbstractEvent
|
62
|
+
def self.payload_store_fields
|
63
|
+
%w[description]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class ExtractLargePayload
|
68
|
+
class << self
|
69
|
+
def serialize(event)
|
70
|
+
return if event.fields_with_large_payloads.empty?
|
71
|
+
|
72
|
+
event.fields_with_large_payloads.each do |field, value|
|
73
|
+
# Extract fields with large payload asynchronously.
|
74
|
+
event.data[field] = extract_large_payload_async(field, value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def deserialize(event)
|
79
|
+
# Load real values for large payload fields. You can use self.class.payload_store_fields here, but then you would require
|
80
|
+
# the event definition in each mircoservice you load this event
|
81
|
+
event.data.select { |k, v| v.start_with?('large-payload:') }.each do |field, value|
|
82
|
+
event.data[field] = resolve_large_payload(value.delete('large-payload:'))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def extract_large_payload_async(field_name, value)
|
89
|
+
payload_key = "large-payload:#{field_name}-#{Digest::MD5.hexdigest(value)}"
|
90
|
+
Thread.new do
|
91
|
+
Faraday.post(
|
92
|
+
"https://my.awesome.api/api/extract_large_payload",
|
93
|
+
{ payload_key: payload_key, value: value }
|
94
|
+
)
|
95
|
+
end
|
96
|
+
payload_key
|
97
|
+
end
|
98
|
+
|
99
|
+
def resolve_large_payload(payload_key)
|
100
|
+
JSON.parse(Faraday.get("https://my.awesome.api/api/large_payload", { payload_key: payload_key }).body)['value']
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Configure our middlewares
|
106
|
+
PgEventstore.configure do |config|
|
107
|
+
config.middlewares = { extract_large_payload: ExtractLargePayload }
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
Now you can use it as follows:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
event = DescriptionChangedEvent.new(data: { 'description' => 'some description' })
|
115
|
+
stream = PgEventstore::Stream.new(context: 'ctx', stream_name: 'some-stream', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
116
|
+
PgEventstore.client.append_to_stream(stream, event)
|
117
|
+
# => #<DescriptionChangedEvent:0x0 @data={"description"=>"large-payload:description-7815696ecbf1c96e6894b779456d330e"}, ...>
|
118
|
+
```
|
119
|
+
|
120
|
+
But when you read it next time, it will automatically resolve the `description` value:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
stream = PgEventstore::Stream.new(context: 'ctx', stream_name: 'some-stream', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
|
124
|
+
PgEventstore.client.read(stream).last
|
125
|
+
# => #<DescriptionChangedEvent:0x0 @data={"description"=>"some description"}, ...>
|
126
|
+
```
|
127
|
+
|
128
|
+
## Remarks
|
129
|
+
|
130
|
+
It is important to know that `pg_eventstore` may retry commands. In that case `#serialize` and `#deserialize` methods may also be retried. You have to make sure that the implementation of `#serialize` and `#deserialize` always returns the same result for the same input, and it does not create duplications. Let's look at the `#serialize` implementation from the example above:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
def serialize(event)
|
134
|
+
return if event.fields_with_large_payloads.empty?
|
135
|
+
|
136
|
+
event.fields_with_large_payloads.each do |field, value|
|
137
|
+
# Extract fields with large payload asynchronously.
|
138
|
+
event.data[field] = extract_large_payload_async(field, value)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def extract_large_payload_async(field_name, value)
|
145
|
+
payload_key = "large-payload:#{field_name}-#{Digest::MD5.hexdigest(value)}"
|
146
|
+
Thread.new do
|
147
|
+
Faraday.post(
|
148
|
+
"https://my.awesome.api/api/extract_large_payload",
|
149
|
+
{ payload_key: payload_key, value: value }
|
150
|
+
)
|
151
|
+
end
|
152
|
+
payload_key
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
Private method `#extract_large_payload_async` should return the same result when passing the same `field_name` and `value` arguments values, and `POST https://my.awesome.api/api/extract_large_payload` may not want to produce duplicates when called multiple times with the same payload.
|
157
|
+
|
158
|
+
## Async vs Sync implementation
|
159
|
+
|
160
|
+
You may notice that the extracting of a large payload is asynchronous in the example above. It is recommended approach of the implementation of `#serialize` method to increase overall performance. But if it hard for you to guarantee the persistence of a payload value - you can go with sync approach, thus not allowing event to be persisted if a payload extraction request fails.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class AbstractCommand
|
6
|
+
attr_reader :queries
|
7
|
+
private :queries
|
8
|
+
|
9
|
+
# @param queries [PgEventstore::Queries]
|
10
|
+
def initialize(queries)
|
11
|
+
@queries = queries
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(*, **)
|
15
|
+
raise NotImplementedError, "Implement #call in your child class."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'commands'
|
4
|
+
require_relative 'event_serializer'
|
5
|
+
require_relative 'pg_result_deserializer'
|
6
|
+
require_relative 'queries'
|
7
|
+
|
8
|
+
module PgEventstore
|
9
|
+
class Client
|
10
|
+
attr_reader :config
|
11
|
+
private :config
|
12
|
+
|
13
|
+
# @param config [PgEventstore::Config]
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
end
|
17
|
+
|
18
|
+
# Append the event or multiple events to the stream. This operation is atomic, meaning that no other event can be
|
19
|
+
# appended by parallel process between the given events.
|
20
|
+
# @param stream [PgEventstore::Stream]
|
21
|
+
# @param events_or_event [PgEventstore::Event, Array<PgEventstore::Event>]
|
22
|
+
# @param options [Hash]
|
23
|
+
# @option options [Integer] :expected_revision provide your own revision number
|
24
|
+
# @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
|
25
|
+
# @param middlewares [Array, nil] provide a list of middleware names to override a config's middlewares
|
26
|
+
# @return [PgEventstore::Event, Array<PgEventstore::Event>] persisted event(s)
|
27
|
+
# @raise [PgEventstore::WrongExpectedRevisionError]
|
28
|
+
def append_to_stream(stream, events_or_event, options: {}, middlewares: nil)
|
29
|
+
result =
|
30
|
+
Commands::Append.new(queries(middlewares(middlewares))).call(stream, *events_or_event, options: options)
|
31
|
+
events_or_event.is_a?(Array) ? result : result.first
|
32
|
+
end
|
33
|
+
|
34
|
+
# Allows you to make several different commands atomic by wrapping then into a block. Order of events, produced by
|
35
|
+
# multiple commands, belonging to different streams - is unbreakable. So, if you append event1 to stream1 and
|
36
|
+
# event2 to stream2 using this method, then thet appear in the same order in the "all" stream.
|
37
|
+
# Example:
|
38
|
+
# PgEventstore.client.multiple do
|
39
|
+
# PgEventstore.client.read(...)
|
40
|
+
# PgEventstore.client.append_to_stream(...)
|
41
|
+
# PgEventstore.client.append_to_stream(...)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# @return the result of the given block
|
45
|
+
def multiple(&blk)
|
46
|
+
Commands::Multiple.new(queries(middlewares)).call(&blk)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Read events from the specific stream or from "all" stream.
|
50
|
+
# @param stream [PgEventstore::Stream]
|
51
|
+
# @param options [Hash] request options
|
52
|
+
# @option options [String] :direction read direction - 'Forwards' or 'Backwards'
|
53
|
+
# @option options [Integer] :from_revision a starting revision number. **Use this option when stream name is a
|
54
|
+
# normal stream name**
|
55
|
+
# @option options [Integer, Symbol] :from_position a starting global position number. **Use this option when reading
|
56
|
+
# from "all" stream**
|
57
|
+
# @option options [Integer] :max_count max number of events to return in one response. Defaults to config.max_count
|
58
|
+
# @option options [Boolean] :resolve_link_tos When using projections to create new events you
|
59
|
+
# can set whether the generated events are pointers to existing events. Setting this option to true tells
|
60
|
+
# PgEventstore to return the original event instead a link event.
|
61
|
+
# @option options [Hash] :filter provide it to filter events. You can filter by: stream and by event type. Filtering
|
62
|
+
# by stream is only available when reading from "all" stream.
|
63
|
+
# Examples:
|
64
|
+
# # Filtering by stream's context. This will return all events which #context is 'User
|
65
|
+
# PgEventstore.client.read(
|
66
|
+
# PgEventstore::Stream.all_stream,
|
67
|
+
# options: { filter: { streams: [{ context: 'User' }] } }
|
68
|
+
# )
|
69
|
+
#
|
70
|
+
# # Filtering by several stream's contexts. This will return all events which #context is either 'User' or
|
71
|
+
# # 'Profile'
|
72
|
+
# PgEventstore.client.read(
|
73
|
+
# PgEventstore::Stream.all_stream,
|
74
|
+
# options: { filter: { streams: [{ context: 'User' }, { context: 'Profile' }] } }
|
75
|
+
# )
|
76
|
+
#
|
77
|
+
# # Filtering by a mix of specific stream and a context. This will return all events which #context is 'User' or
|
78
|
+
# # events belonging to the stream with { context: 'Profile', stream_name: 'ProfileFields', stream_id: '123' }
|
79
|
+
# PgEventstore.client.read(
|
80
|
+
# PgEventstore::Stream.all_stream,
|
81
|
+
# options: {
|
82
|
+
# filter: {
|
83
|
+
# streams: [
|
84
|
+
# { context: 'User' },
|
85
|
+
# { context: 'Profile', stream_name: 'ProfileFields', stream_id: '123' }
|
86
|
+
# ]
|
87
|
+
# }
|
88
|
+
# }
|
89
|
+
# )
|
90
|
+
#
|
91
|
+
# # Filtering the a mix of context and event type
|
92
|
+
# PgEventstore.client.read(
|
93
|
+
# PgEventstore::Stream.all_stream,
|
94
|
+
# options: { filter: { streams: [{ context: 'User' }], event_types: ['MyAwesomeEvent'] } }
|
95
|
+
# )
|
96
|
+
#
|
97
|
+
# # Filtering by specific event when reading from the specific stream
|
98
|
+
# PgEventstore.client.read(stream, options: { filter: { event_types: ['MyAwesomeEvent'] } })
|
99
|
+
# @param middlewares [Array, nil] provide a list of middleware names to override a config's middlewares
|
100
|
+
# @return [Array<PgEventstore::Event>]
|
101
|
+
# @raise [PgEventstore::StreamNotFoundError]
|
102
|
+
def read(stream, options: {}, middlewares: nil)
|
103
|
+
Commands::Read.
|
104
|
+
new(queries(middlewares(middlewares))).
|
105
|
+
call(stream, options: { max_count: config.max_count }.merge(options))
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# @param middlewares [Array, nil]
|
111
|
+
# @return [Array<Object<#serialize, #deserialize>>]
|
112
|
+
def middlewares(middlewares = nil)
|
113
|
+
return config.middlewares.values unless middlewares
|
114
|
+
|
115
|
+
config.middlewares.slice(*middlewares).values
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [PgEventstore::Connection]
|
119
|
+
def connection
|
120
|
+
PgEventstore.connection(config.name)
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param middlewares [Array<Object<#serialize, #deserialize>>]
|
124
|
+
# @return [PgEventstore::Queries]
|
125
|
+
def queries(middlewares)
|
126
|
+
Queries.new(
|
127
|
+
connection,
|
128
|
+
EventSerializer.new(middlewares),
|
129
|
+
PgResultDeserializer.new(middlewares, config.event_class_resolver)
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Commands
|
5
|
+
# @!visibility private
|
6
|
+
class Append < AbstractCommand
|
7
|
+
# @param stream [PgEventstore::Stream]
|
8
|
+
# @param events [Array<PgEventstore::Event>]
|
9
|
+
# @param options [Hash]
|
10
|
+
# @option options [Integer] :expected_revision provide your own revision number
|
11
|
+
# @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
|
12
|
+
# @return [Array<PgEventstore::Event>] persisted events
|
13
|
+
# @raise [PgEventstore::WrongExpectedRevisionError]
|
14
|
+
def call(stream, *events, options: {})
|
15
|
+
raise SystemStreamError, stream if stream.system?
|
16
|
+
|
17
|
+
queries.transaction do
|
18
|
+
stream = queries.find_or_create_stream(stream)
|
19
|
+
revision = stream.stream_revision
|
20
|
+
assert_expected_revision!(revision, options[:expected_revision]) if options[:expected_revision]
|
21
|
+
events.map.with_index(1) do |event, index|
|
22
|
+
queries.insert(stream, prepared_event(event, revision + index))
|
23
|
+
end.tap do
|
24
|
+
queries.update_stream_revision(stream, revision + events.size)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# @param event [PgEventstore::Event]
|
32
|
+
# @param revision [Integer]
|
33
|
+
# @return [PgEventstore::Event]
|
34
|
+
def prepared_event(event, revision)
|
35
|
+
event.class.new(
|
36
|
+
id: event.id, data: event.data, metadata: event.metadata, type: event.type, stream_revision: revision
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param revision [Integer]
|
41
|
+
# @param expected_revision [Symbol, Integer]
|
42
|
+
# @raise [PgEventstore::WrongExpectedRevisionError] in case if revision does not satisfy expected revision
|
43
|
+
# @return [void]
|
44
|
+
def assert_expected_revision!(revision, expected_revision)
|
45
|
+
return if expected_revision == :any
|
46
|
+
|
47
|
+
case [revision, expected_revision]
|
48
|
+
in [Integer, Integer]
|
49
|
+
raise WrongExpectedRevisionError.new(revision, expected_revision) unless revision == expected_revision
|
50
|
+
in [Integer, Symbol]
|
51
|
+
if revision == Stream::INITIAL_STREAM_REVISION && expected_revision == :stream_exists
|
52
|
+
raise WrongExpectedRevisionError.new(revision, expected_revision)
|
53
|
+
end
|
54
|
+
if revision > Stream::INITIAL_STREAM_REVISION && expected_revision == :no_stream
|
55
|
+
raise WrongExpectedRevisionError.new(revision, expected_revision)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Commands
|
5
|
+
# @!visibility private
|
6
|
+
class Read < AbstractCommand
|
7
|
+
# @param stream [PgEventstore::Stream]
|
8
|
+
# @param options [Hash] request options
|
9
|
+
# @option options [String] :direction read direction - 'Forwards' or 'Backwards'
|
10
|
+
# @option options [Integer, Symbol] :from_revision. **Use this option when stream name is a normal stream name**
|
11
|
+
# @option options [Integer, Symbol] :from_position. **Use this option when reading from "all" stream**
|
12
|
+
# @option options [Integer] :max_count
|
13
|
+
# @option options [Boolean] :resolve_link_tos
|
14
|
+
# @option options [Hash] :filter provide it to filter events
|
15
|
+
# @return [Array<PgEventstore::Event>]
|
16
|
+
# @raise [PgEventstore::StreamNotFoundError]
|
17
|
+
def call(stream, options: {})
|
18
|
+
stream = queries.find_stream(stream) || raise(StreamNotFoundError, stream) unless stream.all_stream?
|
19
|
+
|
20
|
+
queries.stream_events(stream, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class Config
|
5
|
+
include Extensions::OptionsExtension
|
6
|
+
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# PostgreSQL connection URI docs https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
|
10
|
+
option(:pg_uri) { 'postgresql://postgres:postgres@localhost:5432/eventstore' }
|
11
|
+
option(:max_count) { 1000 }
|
12
|
+
option(:middlewares) { {} }
|
13
|
+
# Object that responds to #call. Should accept a string and return a class
|
14
|
+
option(:event_class_resolver) { EventClassResolver.new }
|
15
|
+
option(:connection_pool_size) { 5 }
|
16
|
+
option(:connection_pool_timeout) { 5 } # seconds
|
17
|
+
|
18
|
+
# @param name [Symbol] config's name. Its value matches the appropriate key in PgEventstore.config hash
|
19
|
+
def initialize(name:, **options)
|
20
|
+
super
|
21
|
+
@name = name
|
22
|
+
end
|
23
|
+
|
24
|
+
# Computes a value for usage in PgEventstore::Connection
|
25
|
+
# @return [Hash]
|
26
|
+
def connection_options
|
27
|
+
{ uri: pg_uri, pool_size: connection_pool_size, pool_timeout: connection_pool_timeout }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|