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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/docs/reading_events.md +98 -0
- data/lib/pg_eventstore/client.rb +22 -4
- data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +69 -0
- data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +31 -0
- data/lib/pg_eventstore/commands.rb +2 -0
- data/lib/pg_eventstore/errors.rb +10 -0
- data/lib/pg_eventstore/partition.rb +23 -0
- data/lib/pg_eventstore/queries/event_queries.rb +18 -0
- data/lib/pg_eventstore/queries/partition_queries.rb +21 -0
- data/lib/pg_eventstore/queries.rb +2 -0
- data/lib/pg_eventstore/query_builders/basic_filtering.rb +27 -0
- data/lib/pg_eventstore/query_builders/events_filtering.rb +47 -31
- data/lib/pg_eventstore/query_builders/partitions_filtering.rb +83 -0
- data/lib/pg_eventstore/sql_builder.rb +10 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +2 -2
- data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +3 -3
- data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +1 -9
- data/lib/pg_eventstore/utils.rb +27 -8
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +39 -10
- data/lib/pg_eventstore/web/paginator/helpers.rb +11 -3
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +41 -4
- data/lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css +12 -0
- data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +5 -1
- data/lib/pg_eventstore/web/views/home/partials/events.erb +5 -5
- data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +15 -3
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +2 -2
- data/lib/pg_eventstore.rb +1 -0
- data/sig/pg_eventstore/client.rbs +2 -0
- data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +16 -0
- data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +8 -0
- data/sig/pg_eventstore/errors.rbs +8 -0
- data/sig/pg_eventstore/partition.rbs +15 -0
- data/sig/pg_eventstore/queries/event_queries.rbs +2 -0
- data/sig/pg_eventstore/queries/partition_queries.rbs +6 -0
- data/sig/pg_eventstore/query_builders/basic_filtering.rbs +15 -0
- data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +17 -17
- data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +21 -0
- data/sig/pg_eventstore/sql_builder.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +2 -2
- data/sig/pg_eventstore/utils.rbs +4 -0
- data/sig/pg_eventstore/web/application.rbs +6 -0
- data/sig/pg_eventstore/web/paginator/helpers.rbs +2 -0
- 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 [
|
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 [
|
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)
|
data/lib/pg_eventstore/utils.rb
CHANGED
@@ -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:
|
51
|
-
message:
|
52
|
-
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
|
-
|
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
|
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
|
@@ -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
|
-
|
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
|
-
|
40
|
-
events
|
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:
|
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: {
|
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
|
-
|
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="
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|