pg_eventstore 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +90 -0
  6. data/db/extensions.sql +2 -0
  7. data/db/indexes.sql +13 -0
  8. data/db/primary_and_foreign_keys.sql +11 -0
  9. data/db/tables.sql +21 -0
  10. data/docs/appending_events.md +170 -0
  11. data/docs/configuration.md +82 -0
  12. data/docs/events_and_streams.md +45 -0
  13. data/docs/multiple_commands.md +46 -0
  14. data/docs/reading_events.md +161 -0
  15. data/docs/writing_middleware.md +160 -0
  16. data/lib/pg_eventstore/abstract_command.rb +18 -0
  17. data/lib/pg_eventstore/client.rb +133 -0
  18. data/lib/pg_eventstore/commands/append.rb +61 -0
  19. data/lib/pg_eventstore/commands/multiple.rb +14 -0
  20. data/lib/pg_eventstore/commands/read.rb +24 -0
  21. data/lib/pg_eventstore/commands.rb +6 -0
  22. data/lib/pg_eventstore/config.rb +30 -0
  23. data/lib/pg_eventstore/connection.rb +97 -0
  24. data/lib/pg_eventstore/errors.rb +107 -0
  25. data/lib/pg_eventstore/event.rb +59 -0
  26. data/lib/pg_eventstore/event_class_resolver.rb +17 -0
  27. data/lib/pg_eventstore/event_serializer.rb +27 -0
  28. data/lib/pg_eventstore/extensions/options_extension.rb +103 -0
  29. data/lib/pg_eventstore/middleware.rb +15 -0
  30. data/lib/pg_eventstore/pg_result_deserializer.rb +48 -0
  31. data/lib/pg_eventstore/queries.rb +127 -0
  32. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +187 -0
  33. data/lib/pg_eventstore/rspec/has_option_matcher.rb +90 -0
  34. data/lib/pg_eventstore/sql_builder.rb +126 -0
  35. data/lib/pg_eventstore/stream.rb +72 -0
  36. data/lib/pg_eventstore/tasks/setup.rake +37 -0
  37. data/lib/pg_eventstore/version.rb +5 -0
  38. data/lib/pg_eventstore.rb +85 -0
  39. data/pg_eventstore.gemspec +40 -0
  40. 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class Multiple < AbstractCommand
7
+ def call(&blk)
8
+ queries.transaction do
9
+ yield
10
+ end
11
+ end
12
+ end
13
+ end
14
+ 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract_command'
4
+ require_relative 'commands/append'
5
+ require_relative 'commands/read'
6
+ require_relative 'commands/multiple'
@@ -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