pg_eventstore 1.6.0 → 1.8.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +45 -0
  4. data/db/migrations/7_support_reading_streams_system_stream.sql +2 -0
  5. data/db/migrations/8_improve_0_revision_partial_index.sql +2 -0
  6. data/docs/admin_ui.md +7 -1
  7. data/docs/events_and_streams.md +18 -2
  8. data/docs/multiple_commands.md +4 -0
  9. data/docs/reading_events.md +16 -1
  10. data/lib/pg_eventstore/commands/read.rb +1 -1
  11. data/lib/pg_eventstore/queries/event_queries.rb +1 -10
  12. data/lib/pg_eventstore/query_builders/events_filtering.rb +27 -0
  13. data/lib/pg_eventstore/rspec/has_option_matcher.rb +4 -28
  14. data/lib/pg_eventstore/stream.rb +8 -0
  15. data/lib/pg_eventstore/version.rb +1 -1
  16. data/lib/pg_eventstore/web/application.rb +30 -8
  17. data/lib/pg_eventstore/web/paginator/events_collection.rb +20 -8
  18. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +24 -2
  19. data/lib/pg_eventstore/web/views/home/dashboard.erb +9 -0
  20. data/lib/pg_eventstore/web/views/home/partials/events.erb +5 -2
  21. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +15 -0
  22. data/lib/pg_eventstore/web/views/layouts/application.erb +1 -1
  23. data/lib/pg_eventstore.rb +9 -6
  24. data/sig/pg_eventstore/queries/event_queries.rbs +0 -5
  25. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +6 -0
  26. data/sig/pg_eventstore/stream.rbs +3 -0
  27. data/sig/pg_eventstore/web/application.rbs +32 -0
  28. data/sig/pg_eventstore/web/paginator/base_collection.rbs +0 -9
  29. data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +9 -0
  30. data/sig/pg_eventstore/web/paginator/events_collection.rbs +3 -1
  31. data/sig/pg_eventstore.rbs +2 -0
  32. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 337db5e3df424d6d72b3e08367a730af7c77ebfd907d38b309309e30a563e81e
4
- data.tar.gz: 71c36ea38c4d756adea73939ab15e75a2b4500d17768416e0f37013160f0b696
3
+ metadata.gz: ac379ed7a4c1c8ca46edd8e4ec537dafd575dd50749a140d15c49c24a1ece1c1
4
+ data.tar.gz: c5f83c5aa8fccc6f7100172270c951fda96ca7926c7a1b6b37f90217024e3e09
5
5
  SHA512:
6
- metadata.gz: 70699c2d05ddb69f64922c6e0e67d46a523b5ed2f70d9278134435a144bc002013fe0657a408a0657ccb9f0fe2d1f9cd7a1521a50e128375b1c1d8ec1e127584
7
- data.tar.gz: c1125f8dc9af55a1ce1752b94a9bb3273e1153c70e726845867bb91cad9a4b407bcc63537d2bb5859bef20508ce5bfb4391ed56218a690bef2119cf1351597d7
6
+ metadata.gz: 6ffbe0ddbfff6207069a527c7f41693e59d00a3217f68697c851fb2582300fefc569be8035f30924a225b95522ee5ef29ec26c9c6134fe705b31f6b32d0c39a3
7
+ data.tar.gz: 3b1860f5f351f5ffcd56f09c557661796a98f46a49c9a9bf8945667074ad8705f7cb9998639031fe07a4a2dfb89a0bf636b1e8157a21374da869cf4e76377792
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.8.0]
4
+ - Introduce default config for admin web UI. Now if you define `:admin_web_ui` config - it will be preferred over default config
5
+ - Fix pagination of events in admin UI
6
+ - Improve partial index for `$streams` system stream
7
+
8
+ ## [1.7.0]
9
+ - Implement reading from `"$streams"` system stream
10
+ - Disable Host authorization introduced in sinatra v4.1
11
+
3
12
  ## [1.6.0]
4
13
  - Introduce subscriptions CLI. Type `pg-eventstore subscriptions --help` to see available commands. The main purpose of it is to provide the single way to start/stop subscription processes. Check [Subscriptions](docs/subscriptions.md#creating-a-subscription) docs about the new way to start and keep running a subscriptions process.
5
14
 
data/README.md CHANGED
@@ -53,6 +53,51 @@ Documentation chapters:
53
53
 
54
54
  The gem is shipped with its own CLI. Use `pg-eventstore --help` to find out its capabilities.
55
55
 
56
+ ## RSpec
57
+
58
+ ### Clean up test db
59
+
60
+ The gem provides a class to clean up your `pg_eventstore` test db between tests. Example usage(in your `spec/spec_helper.rb`:
61
+
62
+ ```ruby
63
+ require 'pg_eventstore/rspec/test_helpers'
64
+
65
+ RSpec.configure do |config|
66
+ config.before do
67
+ PgEventstore::TestHelpers.clean_up_db
68
+ end
69
+ end
70
+ ```
71
+
72
+ ### RSpec matcher for OptionsExtension
73
+
74
+ If you would like to be able to test the functional, provided by `PgEventstore::Extensions::OptionsExtension` extension - there is a rspec matcher. Load custom matcher in you `spec_helper.rb`:
75
+
76
+ ```ruby
77
+ require 'pg_eventstore/rspec/has_option_matcher'
78
+ ```
79
+
80
+ Let's say you have next class:
81
+ ```ruby
82
+ class SomeClass
83
+ include PgEventstore::Extensions::OptionsExtension
84
+
85
+ option(:some_opt, metadata: { foo: :bar }) { '1' }
86
+ end
87
+ ```
88
+
89
+ To test that its instance has the proper option with the proper default value and proper metadata you can use this matcher:
90
+ ```ruby
91
+ RSpec.describe SomeClass do
92
+ subject { described_class.new }
93
+
94
+ # Check that :some_opt is present
95
+ it { is_expected.to have_option(:some_opt) }
96
+ # Check that :some_opt is present and has the correct default value
97
+ it { is_expected.to have_option(:some_opt).with_default_value('1').with_metadata(foo: :bar) }
98
+ end
99
+ ```
100
+
56
101
  ## Development
57
102
 
58
103
  After checking out the repo, run:
@@ -0,0 +1,2 @@
1
+ CREATE INDEX idx_events_0_stream_revision_global_position ON events USING btree (stream_revision, global_position) WHERE stream_revision = 0;
2
+ CREATE VIEW "$streams" AS SELECT * FROM events WHERE stream_revision = 0;
@@ -0,0 +1,2 @@
1
+ DROP INDEX IF EXISTS idx_events_0_stream_revision_global_position;
2
+ CREATE INDEX idx_events_0_stream_revision_global_position ON events USING btree (global_position) WHERE stream_revision = 0;
data/docs/admin_ui.md CHANGED
@@ -10,7 +10,13 @@
10
10
 
11
11
  ## Authorization
12
12
 
13
- Admin UI is implemented as a rack application. It doesn't have any built-in authentication/authorization mechanism - it is your responsibility to take care of it.
13
+ Admin UI is implemented as a rack application. It doesn't have any built-in authentication/authorization mechanism - it is your responsibility to take care of it. Admin UI tries to look for `:admin_web_ui` config with a fallback to `:default` config. Thus, you can setup Admin UI-specific config, e.g. without some middlewares or so. Example:
14
+
15
+ ```ruby
16
+ PgEventstore.configure(name: :admin_web_ui) do |config|
17
+ config.middlewares = { my_admin_ui_middleware: AdminUIMiddleware.new }
18
+ end
19
+ ```
14
20
 
15
21
  ### Rails integration
16
22
 
@@ -16,7 +16,9 @@
16
16
  - `stream_revision` - Integer(optional, read only). A revision of an event inside its stream.
17
17
  - `data` - Hash(optional). Event's payload data. For example, if you have a `DescriptionChanged` event class, then you may want to have a description value in the event payload data. Example: `DescriptionChanged.new(data: { 'description' => 'Description of something', 'post_id' => SecureRandom.uuid })`
18
18
  - `metadata` - Hash(optional). Event metadata. Event meta information which is not part of an events data payload. Example: `{ published_by: publishing_user.id }`
19
- - `link_id` - String(UUIDv4, optional, read only). If an event is a link event (link events are pointers to other events), this attribute contains the `id` of the original event. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
19
+ - `link_id` - String(UUIDv4, optional, read only). If an event is a link event (link events are pointers to other events), this attribute contains an `id` of the original event. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
20
+ - `link_partition_id` - Integer(optional, read only). If an event is a link event - this attribute contains a partition `id` of original event. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
21
+ - `link` - PgEventstore::Event(optional, read only). When reading from a stream using `resolve_link_tos: true`, if an event is resolved from a link - this attribute contains a `PgEventstore::Event` object which corresponds to that link. Manually assigning this attribute has no effect. It is internally set when reading events from the database.
20
22
  - `created_at` - Time(optional, read only). Database's timestamp when an event was appended to a stream. You may want to put your own timestamp into a `metadata` attribute - it may be useful when migrating between different databases. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
21
23
 
22
24
  Example:
@@ -40,7 +42,21 @@ PgEventstore::Stream.new(context: 'Sales', stream_name: 'Customer', stream_id: '
40
42
  PgEventstore::Stream.new(context: 'Sales', stream_name: 'Customer', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
41
43
  ```
42
44
 
43
- There is a special stream, called the "all" stream. You can get this object by calling the`PgEventstore::Stream.all_stream` method. Read more about the "all" stream in the `Reading from the "all" stream` section of [Reading events](reading_events.md) chapter.
45
+ ### "all" stream
46
+
47
+ There is a special stream, called the "all" stream. You can get this object by calling the `PgEventstore::Stream.all_stream` method. Read more about the "all" stream in the `Reading from the "all" stream` section of [Reading events](reading_events.md) chapter.
48
+
49
+ ### System streams
50
+
51
+ System stream is a special stream, the representation of which is pre-defined by the gem. System stream object can be created in next way:
52
+
53
+ ```ruby
54
+ PgEventstore::Stream.system_stream(stream_name)
55
+ ```
56
+
57
+ Current list of system streams is:
58
+
59
+ - `"$streams"`. Reading from this stream will return 0 revision events. This allows effectively loop through a list of streams. Read more in [Reading events](reading_events.md#streams-stream-filtering) chapter.
44
60
 
45
61
  ## Important note
46
62
 
@@ -44,3 +44,7 @@ old_email =
44
44
  # Sending email outside multiple block to prevent potential re-triggering of it
45
45
  UserMailer.notify_email_changed(user.id, old_email: old_email, new_email: user.email).deliver_later
46
46
  ```
47
+
48
+ ## Side effect of internal implementation
49
+
50
+ Please note that when publishing an event with a type as part of a `multiple` block that does not yet exist in the database, the block will run twice as the first attempt to publish will always fail due to the way `append_to_stream` is implemented. Consider this when writing expectations in your tests for example.
@@ -53,7 +53,7 @@ In case a stream with given name does not exist, a `PgEventstore::StreamNotFound
53
53
  ```ruby
54
54
  begin
55
55
  stream = PgEventstore::Stream.new(context: 'non-existing-context', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
56
- PgEventstore.client.read(stream)
56
+ PgEventstore.client.read(stream, options: { max_count: 1 })
57
57
  rescue PgEventstore::StreamNotFoundError => e
58
58
  puts e.message # => Stream #<PgEventstore::Stream:0x01> does not exist.
59
59
  puts e.stream # => #<PgEventstore::Stream:0x01>
@@ -80,6 +80,15 @@ You can read from a specific position of the "all" stream. This is very similar
80
80
  PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { from_position: 9023, direction: 'Backwards' })
81
81
  ```
82
82
 
83
+ ## Reading from "$streams" system stream
84
+
85
+ `"$streams"` is a special stream which consists of events with `stream_revision == 0`. This allows you to effectively query all streams. Example:
86
+
87
+ ```ruby
88
+ stream = PgEventstore::Stream.system_stream("$streams")
89
+ PgEventstore.client.read(stream).map(&:stream) # => array of unique streams
90
+ ```
91
+
83
92
  ## Middlewares
84
93
 
85
94
  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.
@@ -160,6 +169,12 @@ You can also mix filtering by stream's attributes and event types. The result wi
160
169
  PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
161
170
  ```
162
171
 
172
+ ### "$streams" stream filtering
173
+
174
+ When reading from `"$streams"` same rules apply as when reading from "all" stream. For example, read all streams which have `context == "MyAwesomeContext"` and start from events with event type either `"Foo"` or `"Bar"`:
175
+ ```ruby
176
+ PgEventstore.client.read(PgEventstore::Stream.system_stream("$streams"), options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
177
+ ```
163
178
 
164
179
  ## Pagination
165
180
 
@@ -15,7 +15,7 @@ module PgEventstore
15
15
  # @return [Array<PgEventstore::Event>]
16
16
  # @raise [PgEventstore::StreamNotFoundError]
17
17
  def call(stream, options: {})
18
- queries.events.stream_revision(stream) || raise(StreamNotFoundError, stream) unless stream.all_stream?
18
+ queries.events.stream_revision(stream) || raise(StreamNotFoundError, stream) unless stream.system?
19
19
 
20
20
  queries.events.stream_events(stream, options)
21
21
  end
@@ -70,7 +70,7 @@ module PgEventstore
70
70
  # @param options [Hash]
71
71
  # @return [Array<PgEventstore::Event>]
72
72
  def stream_events(stream, options)
73
- exec_params = events_filtering(stream, options).to_exec_params
73
+ exec_params = QueryBuilders::EventsFiltering.events_filtering(stream, options).to_exec_params
74
74
  raw_events = connection.with do |conn|
75
75
  conn.exec_params(*exec_params)
76
76
  end.to_a
@@ -128,15 +128,6 @@ module PgEventstore
128
128
  [sql_rows_for_insert, values]
129
129
  end
130
130
 
131
- # @param stream [PgEventstore::Stream]
132
- # @param options [Hash]
133
- # @return [PgEventstore::EventsFiltering]
134
- def events_filtering(stream, options)
135
- return QueryBuilders::EventsFiltering.all_stream_filtering(options) if stream.all_stream?
136
-
137
- QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options)
138
- end
139
-
140
131
  # @return [PgEventstore::LinksResolver]
141
132
  def links_resolver
142
133
  LinksResolver.new(connection)
@@ -21,6 +21,18 @@ module PgEventstore
21
21
  SUBSCRIPTIONS_OPTIONS = %i[from_position resolve_link_tos filter max_count].freeze
22
22
 
23
23
  class << self
24
+ # @param stream [PgEventstore::Stream]
25
+ # @param options [Hash]
26
+ # @return [PgEventstore::EventsFiltering]
27
+ def events_filtering(stream, options)
28
+ return all_stream_filtering(options) if stream.all_stream?
29
+ if stream.system? && Stream::KNOWN_SYSTEM_STREAMS.include?(stream.context)
30
+ return system_stream_filtering(stream, options)
31
+ end
32
+
33
+ specific_stream_filtering(stream, options)
34
+ end
35
+
24
36
  # @param options [Hash]
25
37
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
26
38
  def subscriptions_events_filtering(options)
@@ -54,6 +66,15 @@ module PgEventstore
54
66
  event_filter.add_stream_direction(options[:direction])
55
67
  event_filter
56
68
  end
69
+
70
+ # @param stream [PgEventstore::Stream] system stream
71
+ # @param options [Hash]
72
+ # @return [PgEventstore::QueryBuilders::EventsFiltering]
73
+ def system_stream_filtering(stream, options)
74
+ all_stream_filtering(options).tap do |event_filter|
75
+ event_filter.set_source(stream.context)
76
+ end
77
+ end
57
78
  end
58
79
 
59
80
  def initialize
@@ -139,6 +160,12 @@ module PgEventstore
139
160
  @sql_builder.to_exec_params
140
161
  end
141
162
 
163
+ # @param table_name [String] system stream view name
164
+ # @return [void]
165
+ def set_source(table_name)
166
+ @sql_builder.from(%{ "#{PG::Connection.escape(table_name)}" events })
167
+ end
168
+
142
169
  private
143
170
 
144
171
  # @param stream_attrs [Hash]
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # This matcher is defined to test options which are defined by using
4
- # EventStoreClient::Extensions::OptionsExtension option. Example:
4
+ # PgEventstore::Extensions::OptionsExtension option. Example:
5
5
  # Let's say you have next class
6
6
  # class SomeClass
7
7
  # include PgEventstore::Extensions::OptionsExtension
8
8
  #
9
- # option(:some_opt) { '1' }
9
+ # option(:some_opt, metadata: { foo: :bar }) { '1' }
10
10
  # end
11
11
  #
12
- # To test that its instance has the proper option with the proper default value you can use this
12
+ # To test that its instance has the proper option with the proper default value and proper metadata you can use this
13
13
  # matcher:
14
14
  # RSpec.describe SomeClass do
15
15
  # subject { described_class.new }
@@ -17,31 +17,7 @@
17
17
  # # Check that :some_opt is present
18
18
  # it { is_expected.to have_option(:some_opt) }
19
19
  # # Check that :some_opt is present and has the correct default value
20
- # it { is_expected.to have_option(:some_opt).with_default_value('1') }
21
- # end
22
- #
23
- # If you have more complex implementation of default value of your option - you should handle it
24
- # customly. For example:
25
- # class SomeClass
26
- # include PgEventstore::Extensions::OptionsExtension
27
- #
28
- # option(:some_opt) { calc_value }
29
- # end
30
- # You could test it like so:
31
- # RSpec.described SomeClass do
32
- # let(:instance) { described_class.new }
33
- #
34
- # describe ':some_opt default value' do
35
- # subject { instance.some_opt }
36
- #
37
- # let(:value) { 'some val' }
38
- #
39
- # before do
40
- # allow(instance).to receive(:calc_value).and_return(value)
41
- # end
42
- #
43
- # it { is_expected.to eq(value) }
44
- # end
20
+ # it { is_expected.to have_option(:some_opt).with_default_value('1').with_metadata(foo: :bar) }
45
21
  # end
46
22
  RSpec::Matchers.define :has_option do |option_name|
47
23
  match do |obj|
@@ -8,6 +8,8 @@ module PgEventstore
8
8
  SYSTEM_STREAM_PREFIX = '$'
9
9
  # @return [Integer]
10
10
  NON_EXISTING_STREAM_REVISION = -1
11
+ # @return [Array<String>]
12
+ KNOWN_SYSTEM_STREAMS = %w[$streams].freeze
11
13
 
12
14
  class << self
13
15
  # Produces "all" stream instance. "all" stream does not represent any specific stream. Instead, it indicates that
@@ -18,6 +20,12 @@ module PgEventstore
18
20
  stream.instance_variable_set(:@all_stream, true)
19
21
  end
20
22
  end
23
+
24
+ # @param name [String]
25
+ # @return [PgEventstore::Stream]
26
+ def system_stream(name)
27
+ new(context: name, stream_name: '', stream_id: '')
28
+ end
21
29
  end
22
30
 
23
31
  # @!attribute context
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @return [String]
5
- VERSION = "1.6.0"
5
+ VERSION = "1.8.0"
6
6
  end
@@ -5,10 +5,16 @@ require 'securerandom'
5
5
  module PgEventstore
6
6
  module Web
7
7
  class Application < Sinatra::Base
8
+ # @return [Symbol]
9
+ DEFAULT_ADMIN_UI_CONFIG = :admin_web_ui
10
+ # @return [String]
11
+ COOKIES_CONFIG_KEY = 'current_config'
12
+
8
13
  set :static_cache_control, [:private, max_age: 86400]
9
14
  set :environment, -> { (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['APP_ENV'])&.to_sym || :development }
10
15
  set :logging, -> { environment == :development || environment == :test }
11
16
  set :erb, layout: :'layouts/application'
17
+ set :host_authorization, { allow_if: ->(_env) { true } }
12
18
 
13
19
  helpers(Paginator::Helpers, Subscriptions::Helpers) do
14
20
  # @return [Array<Hash>, nil]
@@ -19,6 +25,12 @@ module PgEventstore
19
25
  end&.reject { _1.empty? }
20
26
  end
21
27
 
28
+ # @return [String, nil]
29
+ def system_stream
30
+ params in { filter: { system_stream: String => system_stream } }
31
+ system_stream if Stream::KNOWN_SYSTEM_STREAMS.include?(system_stream)
32
+ end
33
+
22
34
  # @return [Array<String>, nil]
23
35
  def events_filter
24
36
  params in { filter: { events: Array => events } }
@@ -27,12 +39,23 @@ module PgEventstore
27
39
 
28
40
  # @return [Symbol]
29
41
  def current_config
30
- config = request.cookies['current_config']&.to_s&.to_sym
31
- PgEventstore.available_configs.include?(config) ? config : :default
42
+ resolve_config_by_name(request.cookies[COOKIES_CONFIG_KEY]&.to_s&.to_sym)
43
+ end
44
+
45
+ # @param config_name [Symbol, nil]
46
+ # @return [Symbol]
47
+ def resolve_config_by_name(config_name)
48
+ existing_config = [config_name, DEFAULT_ADMIN_UI_CONFIG].find do |name|
49
+ PgEventstore.available_configs.include?(name)
50
+ end
51
+
52
+ existing_config || PgEventstore::DEFAULT_CONFIG
32
53
  end
33
54
 
55
+ # @param val [Object]
56
+ # @return [void]
34
57
  def current_config=(val)
35
- response.set_cookie('current_config', { value: val.to_s, http_only: true, same_site: :lax })
58
+ response.set_cookie(COOKIES_CONFIG_KEY, { value: val.to_s, http_only: true, same_site: :lax })
36
59
  end
37
60
 
38
61
  # @return [PgEventstore::Connection]
@@ -40,7 +63,7 @@ module PgEventstore
40
63
  PgEventstore.connection(current_config)
41
64
  end
42
65
 
43
- # @param collection [PgEventstore::Paginator::BaseCollection]
66
+ # @param collection [PgEventstore::Web::Paginator::BaseCollection]
44
67
  # @return [void]
45
68
  def paginated_json_response(collection)
46
69
  halt 200, {
@@ -85,7 +108,8 @@ module PgEventstore
85
108
  options: {
86
109
  filter: { event_types: events_filter, streams: streams_filter },
87
110
  resolve_link_tos: resolve_link_tos?
88
- }
111
+ },
112
+ system_stream: system_stream
89
113
  )
90
114
 
91
115
  if request.xhr?
@@ -127,9 +151,7 @@ module PgEventstore
127
151
  end
128
152
 
129
153
  post '/change_config' do
130
- config = params[:config]&.to_sym
131
- config = :default unless PgEventstore.available_configs.include?(config)
132
- self.current_config = config
154
+ self.current_config = resolve_config_by_name(params[:config]&.to_s&.to_sym)
133
155
  redirect(redirect_back_url(fallback_url: '/'))
134
156
  end
135
157
 
@@ -19,12 +19,22 @@ module PgEventstore
19
19
  # count instead because of the potential performance degradation.
20
20
  MAX_NUMBER_TO_COUNT = 10_000
21
21
 
22
+ # @param config_name [Symbol]
23
+ # @param starting_id [String, Integer, nil]
24
+ # @param per_page [Integer]
25
+ # @param order [Symbol] :asc or :desc
26
+ # @param options [Hash] additional options to filter the collection
27
+ # @param system_stream [String, nil] a name of system stream
28
+ def initialize(config_name, starting_id:, per_page:, order:, options: {}, system_stream: nil)
29
+ super(config_name, starting_id: starting_id, per_page: per_page, order: order, options: options)
30
+ @stream = system_stream ? PgEventstore::Stream.system_stream(system_stream) : PgEventstore::Stream.all_stream
31
+ end
32
+
22
33
  # @return [Array<PgEventstore::Event>]
23
34
  def collection
24
35
  @_collection ||= PgEventstore.client(config_name).read(
25
- PgEventstore::Stream.all_stream,
26
- options: options.merge(from_position: starting_id, max_count: per_page, direction: order),
27
- middlewares: []
36
+ @stream,
37
+ options: options.merge(from_position: starting_id, max_count: per_page, direction: order)
28
38
  )
29
39
  end
30
40
 
@@ -33,7 +43,8 @@ module PgEventstore
33
43
  return unless collection.size == per_page
34
44
 
35
45
  from_position = event_global_position(collection.first)
36
- sql_builder = QueryBuilders::EventsFiltering.all_stream_filtering(
46
+ sql_builder = QueryBuilders::EventsFiltering.events_filtering(
47
+ @stream,
37
48
  options.merge(from_position: from_position, max_count: 1, direction: order)
38
49
  ).to_sql_builder.unselect.select('global_position').offset(per_page)
39
50
  global_position(sql_builder)
@@ -42,12 +53,13 @@ module PgEventstore
42
53
  # @return [Integer, nil]
43
54
  def prev_page_starting_id
44
55
  from_position = event_global_position(collection.first) || starting_id
45
- sql_builder = QueryBuilders::EventsFiltering.all_stream_filtering(
56
+ sql_builder = QueryBuilders::EventsFiltering.events_filtering(
57
+ @stream,
46
58
  options.merge(from_position: from_position, max_count: per_page, direction: order == :asc ? :desc : :asc)
47
59
  ).to_sql_builder.unselect.select('global_position').offset(1)
48
60
  sql, params = sql_builder.to_exec_params
49
61
  sql = "SELECT * FROM (#{sql}) events ORDER BY global_position #{order} LIMIT 1"
50
- PgEventstore.connection.with do |conn|
62
+ connection.with do |conn|
51
63
  conn.exec_params(sql, params)
52
64
  end.to_a.dig(0, 'global_position')
53
65
  end
@@ -57,7 +69,7 @@ module PgEventstore
57
69
  @_total_count ||=
58
70
  begin
59
71
  sql_builder =
60
- QueryBuilders::EventsFiltering.all_stream_filtering(options).
72
+ QueryBuilders::EventsFiltering.events_filtering(@stream, options).
61
73
  to_sql_builder.remove_limit.remove_group.remove_order
62
74
  count = estimate_count(sql_builder)
63
75
  return count if count > MAX_NUMBER_TO_COUNT
@@ -68,7 +80,7 @@ module PgEventstore
68
80
 
69
81
  private
70
82
 
71
- # @param event [PgEventstore::Event]
83
+ # @param event [PgEventstore::Event, nil]
72
84
  # @return [Integer, nil]
73
85
  def event_global_position(event)
74
86
  event&.link&.global_position || event&.global_position
@@ -60,6 +60,13 @@ $(function(){
60
60
  });
61
61
  }
62
62
 
63
+ let initSystemStreamFilterAutocomplete = function($filter){
64
+ let $streamNameSelect = $filter.find('select');
65
+ $streamNameSelect.select2({
66
+ allowClear: true
67
+ });
68
+ }
69
+
63
70
  let initEventTypeFilterAutocomplete = function($filter) {
64
71
  let $eventTypeSelect = $filter.find('select');
65
72
  $eventTypeSelect.select2({
@@ -86,6 +93,8 @@ $(function(){
86
93
  let $filtersForm = $('#filters-form');
87
94
  // Stream filter template
88
95
  let streamFilterTmpl = $('#stream-filter-tmpl').text();
96
+ // System stream filter template
97
+ let systemStreamFilterTmpl = $('#system-stream-filter-tmpl').text();
89
98
  // Event type filter template
90
99
  let eventFilterTmpl = $('#event-type-filter-tmpl').text();
91
100
 
@@ -93,21 +102,34 @@ $(function(){
93
102
  $filtersForm.on('click', '.remove-filter', function(){
94
103
  $(this).parents('.form-row').remove();
95
104
  });
96
- // Add stream filter button
105
+ // "Add stream filter" button
97
106
  $filtersForm.on('click', '.add-stream-filter', function(){
98
107
  let filtersNum = $filtersForm.find('.stream-filters').children().length + '';
99
108
  $filtersForm.find('.stream-filters').append(streamFilterTmpl.replace(/%NUM%/g, filtersNum));
100
109
  initStreamFilterAutocomplete($filtersForm.find('.stream-filters').children().last());
101
110
  });
102
- // Add event type filter button
111
+ // "Add system stream filter" button
112
+ $filtersForm.on('click', '.add-system-stream-filter', function(){
113
+ if($filtersForm.find('.system-stream-filter').children().length > 0)
114
+ return;
115
+
116
+ $filtersForm.find('.system-stream-filter').append(systemStreamFilterTmpl);
117
+ initSystemStreamFilterAutocomplete($filtersForm.find('.system-stream-filter').children().last());
118
+ });
119
+ // "Add event type filter" button
103
120
  $filtersForm.on('click', '.add-event-filter', function(){
104
121
  $filtersForm.find('.event-filters').append(eventFilterTmpl);
105
122
  initEventTypeFilterAutocomplete($filtersForm.find('.event-filters').children().last());
106
123
  });
124
+
107
125
  // Init select2 for stream filters which were initially rendered
108
126
  $filtersForm.find('.stream-filters').children().each(function(){
109
127
  initStreamFilterAutocomplete($(this));
110
128
  });
129
+ // Init select2 for system stream filter which were initially rendered
130
+ $filtersForm.find('.system-stream-filter').children().each(function(){
131
+ initSystemStreamFilterAutocomplete($(this));
132
+ });
111
133
  // Init select2 for event type filters which were initially rendered
112
134
  $filtersForm.find('.event-filters').children().each(function(){
113
135
  initEventTypeFilterAutocomplete($(this));
@@ -1,6 +1,9 @@
1
1
  <script type="text/html" id="stream-filter-tmpl">
2
2
  <%= erb :'home/partials/stream_filter', { layout: false }, { stream: {} } %>
3
3
  </script>
4
+ <script type="text/html" id="system-stream-filter-tmpl">
5
+ <%= erb :'home/partials/system_stream_filter', { layout: false }, { stream: nil } %>
6
+ </script>
4
7
  <script type="text/html" id="event-type-filter-tmpl">
5
8
  <%= erb :'home/partials/event_filter', { layout: false }, { event_type: '' } %>
6
9
  </script>
@@ -27,6 +30,11 @@
27
30
  <form id="filters-form" action="<%= url('/') %>" method="GET" data-parsley-validate class="form-horizontal form-label-left">
28
31
  <input type="hidden" name="per_page" value="<%= params[:per_page].to_i %>">
29
32
  <input type="hidden" name="resolve_link_tos" value="<%= resolve_link_tos? %>">
33
+ <div class="system-stream-filter">
34
+ <% if system_stream %>
35
+ <%= erb :'home/partials/system_stream_filter', { layout: false }, { stream: system_stream } %>
36
+ <% end %>
37
+ </div>
30
38
  <div class="stream-filters">
31
39
  <% streams_filter&.each do |attrs| %>
32
40
  <%= erb :'home/partials/stream_filter', { layout: false }, { stream: attrs } %>
@@ -48,6 +56,7 @@
48
56
  </button>
49
57
  <div class="dropdown-menu">
50
58
  <a class="dropdown-item add-stream-filter" href="javascript: void(0)">Add stream filter</a>
59
+ <a class="dropdown-item add-system-stream-filter" href="javascript: void(0)">Add system stream filter</a>
51
60
  <a class="dropdown-item add-event-filter" href="javascript: void(0)">Add event filter</a>
52
61
  </div>
53
62
  </div>
@@ -6,10 +6,13 @@
6
6
  <td><%= h event.stream.stream_name %></td>
7
7
  <td><a href="<%= stream_path(event) %>"><%= event.stream.stream_id %></a></td>
8
8
  <td>
9
- <%= h event.type %>
9
+ <p class="float-left"><%= h event.type %></p>
10
10
  <% if event.link %>
11
- <i class="fa fa-link"></i>
11
+ <p class="float-left ml-2">
12
+ <i class="fa fa-link"></i>
13
+ </p>
12
14
  <% end %>
15
+ <div class="clearfix"></div>
13
16
  </td>
14
17
  <td><%= event.created_at.strftime('%F %T') %></td>
15
18
  <td><%= event.id %></td>
@@ -0,0 +1,15 @@
1
+ <div class="form-row align-items-center">
2
+ <div class="col-3">
3
+ <select name="filter[system_stream]" class="form-control mb-2" data-placeholder="Select system stream">
4
+ <option></option>
5
+ <% PgEventstore::Stream::KNOWN_SYSTEM_STREAMS.each do |stream_name| %>
6
+ <option value="<%= stream_name %>" <% if stream == stream_name %> selected <% end %>><%= stream_name %></option>
7
+ <% end %>
8
+ </select>
9
+ </div>
10
+ <div class="col-1">
11
+ <a class="btn btn-default remove-filter" href="javascript: void(0);">
12
+ <i class="fa fa-minus-circle"></i>
13
+ </a>
14
+ </div>
15
+ </div>
@@ -87,7 +87,7 @@
87
87
  <div class="dropdown-menu dropdown-usermenu pull-right" aria-labelledby="navbarDropdown">
88
88
  <% PgEventstore.available_configs.each do |config| %>
89
89
  <form action="<%= url('/change_config') %>" method="POST">
90
- <input type="hidden" name="config" value="<%= h config %>">
90
+ <input type="hidden" name="config" value="<%= h config.to_s %>">
91
91
  <button type="submit" class="dropdown-item"><%= h config.inspect %></button>
92
92
  </form>
93
93
  <% end %>
data/lib/pg_eventstore.rb CHANGED
@@ -18,6 +18,9 @@ require_relative 'pg_eventstore/middleware'
18
18
  require_relative 'pg_eventstore/subscriptions/subscriptions_manager'
19
19
 
20
20
  module PgEventstore
21
+ # @return [Symbol]
22
+ DEFAULT_CONFIG = :default
23
+
21
24
  class << self
22
25
  # @!attribute mutex
23
26
  # @return [Thread::Mutex]
@@ -27,7 +30,7 @@ module PgEventstore
27
30
  # Creates a Config if not exists and yields it to the given block.
28
31
  # @param name [Symbol] a name to assign to a config
29
32
  # @return [Object] a result of the given block
30
- def configure(name: :default)
33
+ def configure(name: DEFAULT_CONFIG)
31
34
  mutex.synchronize do
32
35
  @config[name] ||= Config.new(name: name)
33
36
  connection_config_was = @config[name].connection_options
@@ -48,7 +51,7 @@ module PgEventstore
48
51
 
49
52
  # @param name [Symbol]
50
53
  # @return [PgEventstore::Config]
51
- def config(name = :default)
54
+ def config(name = DEFAULT_CONFIG)
52
55
  return @config[name] if @config[name]
53
56
 
54
57
  error_message = <<~TEXT
@@ -64,7 +67,7 @@ module PgEventstore
64
67
  # thread-safe
65
68
  # @param name [Symbol]
66
69
  # @return [PgEventstore::Connection]
67
- def connection(name = :default)
70
+ def connection(name = DEFAULT_CONFIG)
68
71
  mutex.synchronize do
69
72
  @connection[name] ||= Connection.new(**config(name).connection_options)
70
73
  end
@@ -76,7 +79,7 @@ module PgEventstore
76
79
  # @param retries_interval [Integer, nil] a delay between retries of failed SubscriptionsSet
77
80
  # @param force_lock [Boolean] whether to force-lock subscriptions
78
81
  # @return [PgEventstore::SubscriptionsManager]
79
- def subscriptions_manager(config_name = :default, subscription_set:, max_retries: nil, retries_interval: nil,
82
+ def subscriptions_manager(config_name = DEFAULT_CONFIG, subscription_set:, max_retries: nil, retries_interval: nil,
80
83
  force_lock: false)
81
84
  SubscriptionsManager.new(
82
85
  config: config(config_name),
@@ -89,7 +92,7 @@ module PgEventstore
89
92
 
90
93
  # @param name [Symbol]
91
94
  # @return [PgEventstore::Client]
92
- def client(name = :default)
95
+ def client(name = DEFAULT_CONFIG)
93
96
  Client.new(config(name))
94
97
  end
95
98
 
@@ -108,7 +111,7 @@ module PgEventstore
108
111
 
109
112
  # @return [void]
110
113
  def init_variables
111
- @config = { default: Config.new(name: :default) }
114
+ @config = { DEFAULT_CONFIG => Config.new(name: DEFAULT_CONFIG) }
112
115
  @connection = {}
113
116
  @mutex = Thread::Mutex.new
114
117
  end
@@ -31,11 +31,6 @@ module PgEventstore
31
31
  # _@param_ `events`
32
32
  def prepared_statements: (PgEventstore::Stream stream, ::Array[PgEventstore::Event] events) -> ::Array[(::Array[String] | ::Array[Object])]
33
33
 
34
- # _@param_ `stream`
35
- #
36
- # _@param_ `options`
37
- def events_filtering: (PgEventstore::Stream stream, ::Hash[untyped, untyped] options) -> PgEventstore::QueryBuilders::EventsFiltering
38
-
39
34
  def links_resolver: () -> PgEventstore::LinksResolver
40
35
 
41
36
  # Returns the value of attribute connection.
@@ -6,6 +6,10 @@ module PgEventstore
6
6
  SQL_DIRECTIONS: Hash[String | Symbol, String]
7
7
  SUBSCRIPTIONS_OPTIONS: ::Array[Symbol]
8
8
 
9
+ @sql_builder: PgEventstore::SQLBuilder
10
+
11
+ def self.events_filtering: (PgEventstore::Stream stream, ::Hash[untyped, untyped] options) -> PgEventstore::QueryBuilders::EventsFiltering
12
+
9
13
  # _@param_ `options`
10
14
  def self.subscriptions_events_filtering: (::Hash[untyped, untyped] options) -> PgEventstore::QueryBuilders::EventsFiltering
11
15
 
@@ -17,6 +21,8 @@ module PgEventstore
17
21
  # _@param_ `options`
18
22
  def self.specific_stream_filtering: (PgEventstore::Stream stream, ::Hash[untyped, untyped] options) -> PgEventstore::QueryBuilders::EventsFiltering
19
23
 
24
+ def self.system_stream_filtering: (PgEventstore::Stream stream, Hash[untyped, untyped] options) -> PgEventstore::QueryBuilders::EventsFiltering
25
+
20
26
  def initialize: () -> void
21
27
 
22
28
  # _@param_ `context`
@@ -1,10 +1,13 @@
1
1
  module PgEventstore
2
2
  class Stream
3
+ KNOWN_SYSTEM_STREAMS: Array[String]
3
4
  SYSTEM_STREAM_PREFIX: String
4
5
  NON_EXISTING_STREAM_REVISION: Integer
5
6
 
6
7
  def self.all_stream: () -> PgEventstore::Stream
7
8
 
9
+ def self.system_stream: (String) -> PgEventstore::Stream
10
+
8
11
  # _@param_ `context`
9
12
  #
10
13
  # _@param_ `stream_name`
@@ -0,0 +1,32 @@
1
+ module PgEventstore
2
+ module Web
3
+ class Application
4
+ COOKIES_CONFIG_KEY: String
5
+ DEFAULT_ADMIN_UI_CONFIG: Symbol
6
+
7
+ def asset_url: (String path) -> String
8
+
9
+ def connection: -> PgEventstore::Connection
10
+
11
+ def current_config: -> Symbol
12
+
13
+ def current_config=: (untyped val) -> void
14
+
15
+ def events_filter: -> Array[String]?
16
+
17
+ def h: (String text) -> String
18
+
19
+ def paginated_json_response: (PgEventstore::Web::Paginator::BaseCollection collection) -> void
20
+
21
+ def redirect_back_url: (fallback_url: String) -> String
22
+
23
+ def resolve_config_by_name: (Symbol? config_name)-> Symbol
24
+
25
+ def resolve_link_tos?: -> bool
26
+
27
+ def streams_filter: -> Array[Hash[untyped, untyped]]?
28
+
29
+ def system_stream: -> String?
30
+ end
31
+ end
32
+ end
@@ -2,15 +2,6 @@ module PgEventstore
2
2
  module Web
3
3
  module Paginator
4
4
  class BaseCollection
5
- # _@param_ `config_name`
6
- #
7
- # _@param_ `starting_id`
8
- #
9
- # _@param_ `per_page`
10
- #
11
- # _@param_ `order` — :asc or :desc
12
- #
13
- # _@param_ `options` — additional options to filter the collection
14
5
  def initialize: (
15
6
  Symbol config_name,
16
7
  starting_id: (String | Integer)?,
@@ -4,6 +4,15 @@ module PgEventstore
4
4
  class EventTypesCollection < PgEventstore::Web::Paginator::BaseCollection
5
5
  PER_PAGE: Integer
6
6
 
7
+ def initialize: (
8
+ Symbol config_name,
9
+ starting_id: (String | Integer)?,
10
+ per_page: Integer,
11
+ order: Symbol,
12
+ ?options: ::Hash[untyped, untyped],
13
+ ?system_stream: String?
14
+ ) -> void
15
+
7
16
  def collection: () -> ::Array[::Hash[String, String]]
8
17
 
9
18
  def next_page_starting_id: () -> String?
@@ -6,6 +6,8 @@ module PgEventstore
6
6
  PER_PAGE: ::Hash[String, Integer]
7
7
  MAX_NUMBER_TO_COUNT: Integer
8
8
 
9
+ @stream: PgEventstore::Stream
10
+
9
11
  def collection: () -> ::Array[PgEventstore::Event]
10
12
 
11
13
  def next_page_starting_id: () -> Integer?
@@ -15,7 +17,7 @@ module PgEventstore
15
17
  def total_count: () -> Integer
16
18
 
17
19
  # _@param_ `event`
18
- def event_global_position: (PgEventstore::Event event) -> Integer?
20
+ def event_global_position: (PgEventstore::Event? event) -> Integer?
19
21
 
20
22
  # _@param_ `sql_builder`
21
23
  def estimate_count: (PgEventstore::SQLBuilder sql_builder) -> Integer
@@ -1,4 +1,6 @@
1
1
  module PgEventstore
2
+ DEFAULT_CONFIG: Symbol
3
+
2
4
  # _@param_ `name` — a name to assign to a config
3
5
  #
4
6
  # _@return_ — a result of the given block
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_eventstore
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Dzyzenko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
11
+ date: 2025-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -77,6 +77,8 @@ files:
77
77
  - db/migrations/4_create_subscriptions_set_commands.sql
78
78
  - db/migrations/5_partitions.sql
79
79
  - db/migrations/6_add_commands_data.sql
80
+ - db/migrations/7_support_reading_streams_system_stream.sql
81
+ - db/migrations/8_improve_0_revision_partial_index.sql
80
82
  - docs/admin_ui.md
81
83
  - docs/appending_events.md
82
84
  - docs/configuration.md
@@ -226,6 +228,7 @@ files:
226
228
  - lib/pg_eventstore/web/views/home/partials/events.erb
227
229
  - lib/pg_eventstore/web/views/home/partials/pagination_links.erb
228
230
  - lib/pg_eventstore/web/views/home/partials/stream_filter.erb
231
+ - lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb
229
232
  - lib/pg_eventstore/web/views/layouts/application.erb
230
233
  - lib/pg_eventstore/web/views/subscriptions/index.erb
231
234
  - pg_eventstore.gemspec
@@ -331,6 +334,7 @@ files:
331
334
  - sig/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rbs
332
335
  - sig/pg_eventstore/utils.rbs
333
336
  - sig/pg_eventstore/version.rbs
337
+ - sig/pg_eventstore/web/application.rbs
334
338
  - sig/pg_eventstore/web/paginator/base_collection.rbs
335
339
  - sig/pg_eventstore/web/paginator/event_types_collection.rbs
336
340
  - sig/pg_eventstore/web/paginator/events_collection.rbs