pg_eventstore 1.10.0 → 1.12.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/docs/reading_events.md +98 -0
  4. data/lib/pg_eventstore/client.rb +22 -4
  5. data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +69 -0
  6. data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +31 -0
  7. data/lib/pg_eventstore/commands.rb +2 -0
  8. data/lib/pg_eventstore/errors.rb +10 -0
  9. data/lib/pg_eventstore/partition.rb +23 -0
  10. data/lib/pg_eventstore/queries/event_queries.rb +18 -0
  11. data/lib/pg_eventstore/queries/partition_queries.rb +21 -0
  12. data/lib/pg_eventstore/queries.rb +2 -0
  13. data/lib/pg_eventstore/query_builders/basic_filtering.rb +27 -0
  14. data/lib/pg_eventstore/query_builders/events_filtering.rb +47 -31
  15. data/lib/pg_eventstore/query_builders/partitions_filtering.rb +83 -0
  16. data/lib/pg_eventstore/sql_builder.rb +10 -0
  17. data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +2 -2
  18. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +3 -3
  19. data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +1 -9
  20. data/lib/pg_eventstore/utils.rb +27 -8
  21. data/lib/pg_eventstore/version.rb +1 -1
  22. data/lib/pg_eventstore/web/application.rb +39 -10
  23. data/lib/pg_eventstore/web/paginator/helpers.rb +11 -3
  24. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +41 -4
  25. data/lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css +12 -0
  26. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +5 -1
  27. data/lib/pg_eventstore/web/views/home/partials/events.erb +5 -5
  28. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +15 -3
  29. data/lib/pg_eventstore/web/views/subscriptions/index.erb +2 -2
  30. data/lib/pg_eventstore.rb +1 -0
  31. data/sig/pg_eventstore/client.rbs +2 -0
  32. data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +16 -0
  33. data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +8 -0
  34. data/sig/pg_eventstore/errors.rbs +8 -0
  35. data/sig/pg_eventstore/partition.rbs +15 -0
  36. data/sig/pg_eventstore/queries/event_queries.rbs +2 -0
  37. data/sig/pg_eventstore/queries/partition_queries.rbs +6 -0
  38. data/sig/pg_eventstore/query_builders/basic_filtering.rbs +15 -0
  39. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +17 -17
  40. data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +21 -0
  41. data/sig/pg_eventstore/sql_builder.rbs +1 -1
  42. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +2 -2
  43. data/sig/pg_eventstore/utils.rbs +4 -0
  44. data/sig/pg_eventstore/web/application.rbs +6 -0
  45. data/sig/pg_eventstore/web/paginator/helpers.rbs +2 -0
  46. metadata +13 -6
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module QueryBuilders
5
+ # @!visibility private
6
+ class PartitionsFiltering < BasicFiltering
7
+ # @return [String]
8
+ TABLE_NAME = 'partitions'
9
+ private_constant :TABLE_NAME
10
+
11
+ class << self
12
+ # @param options [Hash]
13
+ # @return [Array<String>]
14
+ def extract_event_types_filter(options)
15
+ options in { filter: { event_types: Array => event_types } }
16
+ event_types&.select! do
17
+ _1.is_a?(String)
18
+ end
19
+ event_types || []
20
+ end
21
+
22
+ # @param options [Hash]
23
+ # @return [Array<Hash[Symbol, String]>]
24
+ def extract_streams_filter(options)
25
+ options in { filter: { streams: Array => streams } }
26
+ streams = streams&.map do
27
+ _1 in { context: String | NilClass => context }
28
+ _1 in { stream_name: String | NilClass => stream_name }
29
+ { context: context, stream_name: stream_name }
30
+ end
31
+ streams || []
32
+ end
33
+ end
34
+
35
+ # @return [String]
36
+ def to_table_name
37
+ TABLE_NAME
38
+ end
39
+
40
+ # @param context [String, nil]
41
+ # @param stream_name [String, nil]
42
+ # @return [void]
43
+ def add_stream_attrs(context: nil, stream_name: nil)
44
+ stream_attrs = { context: context, stream_name: stream_name }
45
+ return unless correct_stream_filter?(stream_attrs)
46
+
47
+ stream_attrs.compact!
48
+ sql = stream_attrs.map do |attr, _|
49
+ "#{to_table_name}.#{attr} = ?"
50
+ end.join(" AND ")
51
+ @sql_builder.where_or(sql, *stream_attrs.values)
52
+ end
53
+
54
+ # @param event_types [Array<String>]
55
+ # @return [void]
56
+ def add_event_types(event_types)
57
+ return if event_types.empty?
58
+
59
+ @sql_builder.where("#{to_table_name}.event_type = ANY(?::varchar[])", event_types)
60
+ end
61
+
62
+ # @return [void]
63
+ def with_event_types
64
+ @sql_builder.where('event_type IS NOT NULL')
65
+ end
66
+
67
+ private
68
+
69
+ # @param stream_attrs [Hash]
70
+ # @return [Boolean]
71
+ def correct_stream_filter?(stream_attrs)
72
+ result = (stream_attrs in { context: String, stream_name: String } | { context: String, stream_name: nil })
73
+ return true if result
74
+
75
+ PgEventstore&.logger&.debug(<<~TEXT)
76
+ Ignoring unsupported stream filter format for grouped read #{stream_attrs.compact.inspect}. \
77
+ See docs/reading_events.md docs for supported formats.
78
+ TEXT
79
+ false
80
+ end
81
+ end
82
+ end
83
+ end
@@ -4,6 +4,16 @@ module PgEventstore
4
4
  # Deadly simple SQL builder
5
5
  # @!visibility private
6
6
  class SQLBuilder
7
+ class << self
8
+ # @param builders [Array<PgEventstore::SQLBuilder>]
9
+ # @return [PgEventstore::SQLBuilder]
10
+ def union_builders(builders)
11
+ builders[1..].each_with_object(builders[0]) do |builder, first_builder|
12
+ first_builder.union(builder)
13
+ end
14
+ end
15
+ end
16
+
7
17
  def initialize
8
18
  @select_values = []
9
19
  @from_value = nil
@@ -16,9 +16,9 @@ module PgEventstore
16
16
  callbacks.run_callbacks(:process, Utils.original_global_position(raw_event)) do
17
17
  handler.call(raw_event)
18
18
  end
19
- rescue
19
+ rescue => exception
20
20
  raw_events.unshift(raw_event)
21
- raise
21
+ raise Utils.wrap_exception(exception, global_position: Utils.original_global_position(raw_event))
22
22
  end
23
23
 
24
24
  # @param callbacks [PgEventstore::Callbacks]
@@ -26,7 +26,7 @@ module PgEventstore
26
26
  end
27
27
 
28
28
  # @param subscription [PgEventstore::Subscription]
29
- # @param error [StandardError]
29
+ # @param error [PgEventstore::WrappedException]
30
30
  # @return [void]
31
31
  def update_subscription_error(subscription, error)
32
32
  subscription.update(last_error: Utils.error_info(error), last_error_occurred_at: Time.now.utc)
@@ -36,13 +36,13 @@ module PgEventstore
36
36
  # @param restart_terminator [#call, nil]
37
37
  # @param failed_subscription_notifier [#call, nil]
38
38
  # @param events_processor [PgEventstore::EventsProcessor]
39
- # @param error [StandardError]
39
+ # @param error [PgEventstore::WrappedException]
40
40
  # @return [void]
41
41
  def restart_events_processor(subscription, restart_terminator, failed_subscription_notifier, events_processor,
42
42
  error)
43
43
  return if restart_terminator&.call(subscription.dup)
44
44
  if subscription.restart_count >= subscription.max_restarts_number
45
- return failed_subscription_notifier&.call(subscription.dup, error)
45
+ return failed_subscription_notifier&.call(subscription.dup, Utils.unwrap_exception(error))
46
46
  end
47
47
 
48
48
  Thread.new do
@@ -127,7 +127,7 @@ module PgEventstore
127
127
  def subscriptions_events(query_options)
128
128
  return {} if query_options.empty?
129
129
 
130
- final_builder = union_builders(query_options.map { |id, opts| query_builder(id, opts) })
130
+ final_builder = SQLBuilder.union_builders(query_options.map { |id, opts| query_builder(id, opts) })
131
131
  raw_events = connection.with do |conn|
132
132
  conn.exec_params(*final_builder.to_exec_params)
133
133
  end.to_a
@@ -177,14 +177,6 @@ module PgEventstore
177
177
  builder.select("#{id} as runner_id")
178
178
  end
179
179
 
180
- # @param builders [Array<PgEventstore::SQLBuilder>]
181
- # @return [PgEventstore::SQLBuilder]
182
- def union_builders(builders)
183
- builders[1..].each_with_object(builders[0]) do |builder, first_builder|
184
- first_builder.union(builder)
185
- end
186
- end
187
-
188
180
  # @return [PgEventstore::TransactionQueries]
189
181
  def transaction_queries
190
182
  TransactionQueries.new(connection)
@@ -46,11 +46,14 @@ module PgEventstore
46
46
  # @param error [StandardError]
47
47
  # @return [Hash]
48
48
  def error_info(error)
49
+ original_error = unwrap_exception(error)
49
50
  {
50
- class: error.class,
51
- message: error.message,
52
- backtrace: error.backtrace
53
- }
51
+ class: original_error.class,
52
+ message: original_error.message,
53
+ backtrace: original_error.backtrace
54
+ }.tap do |attrs|
55
+ attrs.merge!(error.extra) if error.is_a?(WrappedException)
56
+ end
54
57
  end
55
58
 
56
59
  # @param str [String]
@@ -82,7 +85,8 @@ module PgEventstore
82
85
  def write_to_file(file_path, content)
83
86
  file = File.open(file_path, "w")
84
87
  file.write(content)
85
- file.close
88
+ ensure
89
+ file&.close
86
90
  end
87
91
 
88
92
  # @param file_path [String]
@@ -96,10 +100,25 @@ module PgEventstore
96
100
  # @return [String, nil]
97
101
  def read_pid(file_path)
98
102
  file = File.open(file_path, "r")
99
- file.readline.strip.tap do
100
- file.close
101
- end
103
+ file.readline.strip
102
104
  rescue Errno::ENOENT
105
+ ensure
106
+ file&.close
107
+ end
108
+
109
+ # @param exception [StandardError]
110
+ # @param extra [Hash] additional exception info
111
+ # @return [PgEventstore::WrappedException]
112
+ def wrap_exception(exception, **extra)
113
+ WrappedException.new(exception, extra)
114
+ end
115
+
116
+ # @param wrapped_exception [StandardError, PgEventstore::WrappedException]
117
+ # @return [StandardError]
118
+ def unwrap_exception(wrapped_exception)
119
+ return wrapped_exception.original_exception if wrapped_exception.is_a?(WrappedException)
120
+
121
+ wrapped_exception
103
122
  end
104
123
  end
105
124
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @return [String]
5
- VERSION = "1.10.0"
5
+ VERSION = "1.12.0"
6
6
  end
@@ -13,6 +13,11 @@ module PgEventstore
13
13
  # @return [String]
14
14
  COOKIES_FLASH_MESSAGE_KEY = 'flash_message'
15
15
 
16
+ # Defines a replacement for empty string value in a stream attributes filter or in an event type filter. This
17
+ # replacement is needed to differentiate a user selection vs default placeholder value.
18
+ # @return [String]
19
+ EMPTY_STRING_SIGN = "\x00".freeze
20
+
16
21
  set :static_cache_control, [:private, max_age: 86400]
17
22
  set :environment, -> { (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['APP_ENV'])&.to_sym || :development }
18
23
  set :logging, -> { environment == :development || environment == :test }
@@ -22,7 +27,7 @@ module PgEventstore
22
27
  helpers(Paginator::Helpers, Subscriptions::Helpers) do
23
28
  # @return [Array<Hash>, nil]
24
29
  def streams_filter
25
- params in { filter: { streams: Array => streams } }
30
+ streams = QueryBuilders::EventsFiltering.extract_streams_filter(params)
26
31
  streams&.select { _1 in { context: String, stream_name: String, stream_id: String } }&.map do
27
32
  Hash[_1.reject { |_, value| value == '' }].transform_keys(&:to_sym)
28
33
  end&.reject { _1.empty? }
@@ -36,8 +41,9 @@ module PgEventstore
36
41
 
37
42
  # @return [Array<String>, nil]
38
43
  def events_filter
39
- params in { filter: { events: Array => events } }
40
- events&.select { _1.is_a?(String) && _1 != '' }
44
+ event_filters = { filter: { event_types: params.dig(:filter, :events) } }
45
+ events = QueryBuilders::EventsFiltering.extract_event_types_filter(event_filters)
46
+ events&.reject { _1 == '' }
41
47
  end
42
48
 
43
49
  # @return [Symbol]
@@ -69,8 +75,11 @@ module PgEventstore
69
75
  # @param collection [PgEventstore::Web::Paginator::BaseCollection]
70
76
  # @return [void]
71
77
  def paginated_json_response(collection)
78
+ results = collection.collection.map do |attrs|
79
+ attrs.transform_values { escape_empty_string(_1) }
80
+ end
72
81
  halt 200, {
73
- results: collection.collection,
82
+ results: results,
74
83
  pagination: { more: !collection.next_page_starting_id.nil?, starting_id: collection.next_page_starting_id }
75
84
  }.to_json
76
85
  end
@@ -108,9 +117,25 @@ module PgEventstore
108
117
  COOKIES_FLASH_MESSAGE_KEY, { value: val, http_only: false, same_site: :lax, path: '/' }
109
118
  )
110
119
  end
120
+
121
+ # @param string [String, nil]
122
+ # @return [String, nil]
123
+ def escape_empty_string(string)
124
+ string == '' ? EMPTY_STRING_SIGN : string
125
+ end
126
+
127
+ # @param string [String, nil]
128
+ # @return [String, nil]
129
+ def unescape_empty_string(string)
130
+ string == EMPTY_STRING_SIGN ? '' : string
131
+ end
111
132
  end
112
133
 
113
134
  get '/' do
135
+ streams_filter = self.streams_filter&.map do |attrs|
136
+ attrs.transform_values { unescape_empty_string(_1) }
137
+ end
138
+ events_filter = self.events_filter&.map(&method(:unescape_empty_string))
114
139
  @collection = Paginator::EventsCollection.new(
115
140
  current_config,
116
141
  starting_id: params[:starting_id]&.to_i,
@@ -169,7 +194,7 @@ module PgEventstore
169
194
  get '/stream_contexts_filtering', provides: :json do
170
195
  collection = Paginator::StreamContextsCollection.new(
171
196
  current_config,
172
- starting_id: params[:starting_id],
197
+ starting_id: unescape_empty_string(params[:starting_id]),
173
198
  per_page: Paginator::StreamContextsCollection::PER_PAGE,
174
199
  order: :asc,
175
200
  options: { query: params[:term] }
@@ -180,10 +205,10 @@ module PgEventstore
180
205
  get '/stream_names_filtering', provides: :json do
181
206
  collection = Paginator::StreamNamesCollection.new(
182
207
  current_config,
183
- starting_id: params[:starting_id],
208
+ starting_id: unescape_empty_string(params[:starting_id]),
184
209
  per_page: Paginator::StreamNamesCollection::PER_PAGE,
185
210
  order: :asc,
186
- options: { query: params[:term], context: params[:context] }
211
+ options: { query: params[:term], context: unescape_empty_string(params[:context]) }
187
212
  )
188
213
  paginated_json_response(collection)
189
214
  end
@@ -191,10 +216,14 @@ module PgEventstore
191
216
  get '/stream_ids_filtering', provides: :json do
192
217
  collection = Paginator::StreamIdsCollection.new(
193
218
  current_config,
194
- starting_id: params[:starting_id],
219
+ starting_id: unescape_empty_string(params[:starting_id]),
195
220
  per_page: Paginator::StreamIdsCollection::PER_PAGE,
196
221
  order: :asc,
197
- options: { query: params[:term], context: params[:context], stream_name: params[:stream_name] }
222
+ options: {
223
+ query: params[:term],
224
+ context: unescape_empty_string(params[:context]),
225
+ stream_name: unescape_empty_string(params[:stream_name])
226
+ }
198
227
  )
199
228
  paginated_json_response(collection)
200
229
  end
@@ -202,7 +231,7 @@ module PgEventstore
202
231
  get '/event_types_filtering', provides: :json do
203
232
  collection = Paginator::EventTypesCollection.new(
204
233
  current_config,
205
- starting_id: params[:starting_id],
234
+ starting_id: unescape_empty_string(params[:starting_id]),
206
235
  per_page: Paginator::EventTypesCollection::PER_PAGE,
207
236
  order: :asc,
208
237
  options: { query: params[:term] }
@@ -88,9 +88,9 @@ module PgEventstore
88
88
  filter: {
89
89
  streams: [
90
90
  {
91
- context: event.stream.context,
92
- stream_name: event.stream.stream_name,
93
- stream_id: event.stream.stream_id
91
+ context: escape_empty_string(event.stream.context),
92
+ stream_name: escape_empty_string(event.stream.stream_name),
93
+ stream_id: escape_empty_string(event.stream.stream_id)
94
94
  }
95
95
  ]
96
96
  }
@@ -98,6 +98,14 @@ module PgEventstore
98
98
  )
99
99
  end
100
100
 
101
+ # @param str [String]
102
+ # @return [String]
103
+ def empty_characters_fallback(str)
104
+ return str unless str.strip == ''
105
+
106
+ '<i>Non-printable characters</i>'
107
+ end
108
+
101
109
  private
102
110
 
103
111
  # @param starting_id [String, Integer, nil]
@@ -1,6 +1,30 @@
1
1
  $(function(){
2
2
  "use strict";
3
3
 
4
+ const EMPTY_STRING_SIGN = "\x00";
5
+
6
+ let templateSelection = function(state){
7
+ if (!state.id)
8
+ return state.text;
9
+
10
+ return filterValueToHuman(state.element && state.element.value, state.text);
11
+ }
12
+
13
+ let templateResult = function(item){
14
+ return filterValueToHuman(item.text);
15
+ }
16
+
17
+ let filterValueToHuman = function(str, alternativeStr){
18
+ if(str === EMPTY_STRING_SIGN)
19
+ return $("<i>Empty String</i>");
20
+
21
+ let result = alternativeStr || str;
22
+ if($.trim(result.replaceAll(EMPTY_STRING_SIGN, '')) === '')
23
+ return $("<i>Non-printable characters</i>");
24
+
25
+ return result;
26
+ }
27
+
4
28
  let initStreamFilterAutocomplete = function($filter) {
5
29
  let $contextSelect = $filter.find('select[name*="context"]');
6
30
  let $streamNameSelect = $filter.find('select[name*="stream_name"]');
@@ -22,7 +46,9 @@ $(function(){
22
46
  return data;
23
47
  },
24
48
  },
25
- allowClear: true
49
+ allowClear: true,
50
+ templateSelection: templateSelection,
51
+ templateResult: templateResult,
26
52
  });
27
53
  $streamNameSelect.select2({
28
54
  ajax: {
@@ -41,7 +67,9 @@ $(function(){
41
67
  return data;
42
68
  },
43
69
  },
44
- allowClear: true
70
+ allowClear: true,
71
+ templateSelection: templateSelection,
72
+ templateResult: templateResult,
45
73
  });
46
74
  $streamIdSelect.select2({
47
75
  ajax: {
@@ -61,7 +89,9 @@ $(function(){
61
89
  return data;
62
90
  },
63
91
  },
64
- allowClear: true
92
+ allowClear: true,
93
+ templateSelection: templateSelection,
94
+ templateResult: templateResult,
65
95
  });
66
96
  $contextSelect.on('change.select2', removeDeleteBtn);
67
97
  $streamNameSelect.on('change.select2', removeDeleteBtn);
@@ -89,7 +119,9 @@ $(function(){
89
119
  return data;
90
120
  },
91
121
  },
92
- allowClear: true
122
+ allowClear: true,
123
+ templateSelection: templateSelection,
124
+ templateResult: templateResult,
93
125
  });
94
126
  }
95
127
 
@@ -130,6 +162,11 @@ $(function(){
130
162
  initEventTypeFilterAutocomplete($filtersForm.find('.event-filters').children().last());
131
163
  });
132
164
 
165
+ // Because zero-byte character can't be rendered into HTML properly - loop through each option, marked as containing
166
+ // zero-byte value and assign zero-byte value explicitly. This must be done before we initialize select2 plugin.
167
+ $('option.zero-byte-val').each(function(){
168
+ this.value = EMPTY_STRING_SIGN;
169
+ });
133
170
  // Init select2 for stream filters which were initially rendered
134
171
  $filtersForm.find('.stream-filters').children().each(function(){
135
172
  initStreamFilterAutocomplete($(this));
@@ -4,6 +4,18 @@ tr.collapsing {
4
4
  display: none;
5
5
  }
6
6
 
7
+ td.json-cell {
8
+ max-width: 1px;
9
+ }
10
+
11
+ td.json-cell pre {
12
+ display: block;
13
+ width: 100%;
14
+ box-sizing: border-box;
15
+ overflow-x: auto;
16
+ white-space: pre;
17
+ }
18
+
7
19
  #confirmation-modal .modal-body {
8
20
  font-size: 1.2rem;
9
21
  }
@@ -3,7 +3,11 @@
3
3
  <select name="filter[events][]" class="form-control mb-2" data-placeholder="Event type" data-url="<%= url('/event_types_filtering') %>" autocomplete="off">
4
4
  <option></option>
5
5
  <% if event_type %>
6
- <option value="<%= h event_type %>" selected><%= h event_type %></option>
6
+ <% if event_type == PgEventstore::Web::Application::EMPTY_STRING_SIGN %>
7
+ <option class="zero-byte-val" selected></option>
8
+ <% else %>
9
+ <option value="<%= h event_type %>" selected><%= h event_type %></option>
10
+ <% end %>
7
11
  <% end %>
8
12
  </select>
9
13
  </div>
@@ -2,17 +2,17 @@
2
2
  <tr>
3
3
  <td><%= event.global_position %></td>
4
4
  <td><%= event.stream_revision %></td>
5
- <td><%= h event.stream.context %></td>
6
- <td><%= h event.stream.stream_name %></td>
5
+ <td><%= empty_characters_fallback(h event.stream.context) %></td>
6
+ <td><%= empty_characters_fallback(h event.stream.stream_name) %></td>
7
7
  <td>
8
- <a href="<%= stream_path(event) %>"><%= h event.stream.stream_id %></a>
8
+ <a href="<%= stream_path(event) %>"><%= empty_characters_fallback(h event.stream.stream_id) %></a>
9
9
  <a role="button" href="#" data-title="Copy stream definition." class="copy-to-clipboard"
10
10
  data-clipboard-content="<%= h "PgEventstore::Stream.new(context: #{event.stream.context.inspect}, stream_name: #{event.stream.stream_name.inspect}, stream_id: #{event.stream.stream_id.inspect})" %>">
11
11
  <i class="fa fa-clipboard"></i>
12
12
  </a>
13
13
  </td>
14
14
  <td>
15
- <p class="float-left"><%= h event.type %></p>
15
+ <p class="float-left"><%= empty_characters_fallback(h event.type) %></p>
16
16
  <% if event.link %>
17
17
  <p class="float-left ml-2">
18
18
  <i class="fa fa-link"></i>
@@ -32,7 +32,7 @@
32
32
  </td>
33
33
  </tr>
34
34
  <tr class="event-payload d-none">
35
- <td colspan="8">
35
+ <td colspan="9" class="json-cell">
36
36
  <strong>Data:</strong>
37
37
  <pre><%= h JSON.pretty_generate(event.data) %></pre>
38
38
  <strong>Metadata:</strong>
@@ -3,7 +3,11 @@
3
3
  <select name="filter[streams][][context]" class="form-control mb-2" data-placeholder="Context" data-url="<%= url('/stream_contexts_filtering') %>" autocomplete="off">
4
4
  <option></option>
5
5
  <% if stream[:context] %>
6
- <option value="<%= h stream[:context] %>" selected><%= h stream[:context] %></option>
6
+ <% if stream[:context] == PgEventstore::Web::Application::EMPTY_STRING_SIGN %>
7
+ <option class="zero-byte-val" selected></option>
8
+ <% else %>
9
+ <option value="<%= h stream[:context] %>" selected><%= h stream[:context] %></option>
10
+ <% end %>
7
11
  <% end %>
8
12
  </select>
9
13
  </div>
@@ -11,7 +15,11 @@
11
15
  <select name="filter[streams][][stream_name]" class="form-control mb-2" data-placeholder="Stream name" data-url="<%= url('/stream_names_filtering') %>" autocomplete="off">
12
16
  <option></option>
13
17
  <% if stream[:stream_name] %>
14
- <option value="<%= h stream[:stream_name] %>" selected><%= h stream[:stream_name] %></option>
18
+ <% if stream[:stream_name] == PgEventstore::Web::Application::EMPTY_STRING_SIGN %>
19
+ <option class="zero-byte-val" selected></option>
20
+ <% else %>
21
+ <option value="<%= h stream[:stream_name] %>" selected><%= h stream[:stream_name] %></option>
22
+ <% end %>
15
23
  <% end %>
16
24
  </select>
17
25
  </div>
@@ -19,7 +27,11 @@
19
27
  <select name="filter[streams][][stream_id]" class="form-control mb-2" data-placeholder="Stream ID" data-url="<%= url('/stream_ids_filtering') %>" autocomplete="off">
20
28
  <option></option>
21
29
  <% if stream[:stream_id] %>
22
- <option value="<%= h stream[:stream_id] %>" selected><%= h stream[:stream_id] %></option>
30
+ <% if stream[:stream_id] == PgEventstore::Web::Application::EMPTY_STRING_SIGN %>
31
+ <option class="zero-byte-val" selected></option>
32
+ <% else %>
33
+ <option value="<%= h stream[:stream_id] %>" selected><%= h stream[:stream_id] %></option>
34
+ <% end %>
23
35
  <% end %>
24
36
  </select>
25
37
  </div>
@@ -210,13 +210,13 @@
210
210
  </td>
211
211
  </tr>
212
212
  <tr class="collapse" id="options-<%= subscription.id %>">
213
- <td colspan="16">
213
+ <td colspan="16" class="json-cell">
214
214
  <pre><%= h JSON.pretty_generate(subscription.options) %></pre>
215
215
  </td>
216
216
  </tr>
217
217
  <% if subscription.last_error %>
218
218
  <tr class="collapse" id="last-error-<%= subscription.id %>">
219
- <td colspan="16">
219
+ <td colspan="16" class="json-cell">
220
220
  <pre><%= h JSON.pretty_generate(subscription.last_error) %></pre>
221
221
  </td>
222
222
  </tr>
data/lib/pg_eventstore.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'pg_eventstore/extensions/callback_handlers_extension'
9
9
  require_relative 'pg_eventstore/extensions/using_connection_extension'
10
10
  require_relative 'pg_eventstore/event_class_resolver'
11
11
  require_relative 'pg_eventstore/config'
12
+ require_relative 'pg_eventstore/partition'
12
13
  require_relative 'pg_eventstore/event'
13
14
  require_relative 'pg_eventstore/stream'
14
15
  require_relative 'pg_eventstore/commands'
@@ -26,6 +26,8 @@ module PgEventstore
26
26
  # _@param_ `middlewares` — provide a list of middleware names to override a config's middlewares
27
27
  def read: (PgEventstore::Stream stream, ?options: ::Hash[untyped, untyped], ?middlewares: ::Array[::Symbol]?) -> ::Array[PgEventstore::Event]
28
28
 
29
+ def read_grouped: (Stream stream, ?options: Hash[untyped, untyped], ?middlewares: ::Array[::Symbol]?) -> ::Array[PgEventstore::Event]
30
+
29
31
  # _@param_ `stream`
30
32
  #
31
33
  # _@param_ `options` — request options
@@ -0,0 +1,16 @@
1
+ module PgEventstore
2
+ module Commands
3
+ class AllStreamReadGrouped < AbstractCommand
4
+
5
+ def call: (Stream stream, ?options: ::Hash[untyped, untyped]) -> Array[Event]
6
+
7
+ private
8
+
9
+ def build_filter_options_for_partitions: (Partition partition, Hash[untyped, untyped] options) -> Hash[untyped, untyped]
10
+
11
+ def build_filter_options_for_streams: (Partition partition, Array[String] stream_ids, Hash[untyped, untyped] options) -> Array[Hash[untyped, untyped]]
12
+
13
+ def group_stream_ids: (Hash[untyped, untyped] options) -> Hash[untyped, untyped]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ module PgEventstore
2
+ module Commands
3
+ class RegularStreamReadGrouped < AbstractCommand
4
+
5
+ def call: (Stream stream, ?options: ::Hash[untyped, untyped]) -> Array[Event]
6
+ end
7
+ end
8
+ end
@@ -123,4 +123,12 @@ module PgEventstore
123
123
 
124
124
  def user_friendly_message: -> String
125
125
  end
126
+
127
+ class WrappedException < PgEventstore::Error
128
+ def initialize: (StandardError original_exception, Hash[Symbol, untyped] extra) -> void
129
+
130
+ attr_accessor original_exception: StandardError
131
+
132
+ attr_accessor extra: Hash[Symbol, untyped]
133
+ end
126
134
  end
@@ -0,0 +1,15 @@
1
+ module PgEventstore
2
+ class Partition
3
+ include Extensions::OptionsExtension
4
+
5
+ attr_accessor id: Integer
6
+
7
+ attr_accessor context: String
8
+
9
+ attr_accessor stream_name: String?
10
+
11
+ attr_accessor event_type: String?
12
+
13
+ attr_accessor table_name: String
14
+ end
15
+ end