pg_eventstore 1.9.0 → 1.11.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 +12 -0
- data/lib/pg_eventstore/errors.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/subscription.rb +4 -1
- data/lib/pg_eventstore/utils.rb +22 -4
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +63 -14
- data/lib/pg_eventstore/web/paginator/helpers.rb +11 -3
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +74 -8
- data/lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css +12 -0
- data/lib/pg_eventstore/web/subscriptions/helpers.rb +31 -17
- data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +5 -1
- data/lib/pg_eventstore/web/views/home/partials/events.erb +11 -5
- data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +15 -3
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +9 -7
- data/sig/pg_eventstore/errors.rbs +8 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +2 -2
- data/sig/pg_eventstore/subscriptions/subscription.rbs +2 -0
- 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
- data/sig/pg_eventstore/web/subscriptions/helpers.rbs +2 -0
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 402dc90a012b5e1ee934da4f62c4af4ba62e69693a45dac1a84d461950da5717
|
4
|
+
data.tar.gz: 6c5d1fdc2e9475eca72b5ef27d0081e0aec6331f76639b7172e676d8b6e63e3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b20c1124f2108b671bd28f3677fb3b2ae88f3b19d2470eb5a31102eb597d4bd5af1080f9bd337b1528107eef2e4bce6b882d434aa8536abc0a5a80b373bfe3a3
|
7
|
+
data.tar.gz: 9fd5af079cedc277e278b2e0b50e628601880a81ec5bf7c94700595f6472be389b2fa3d3a2ba2f5d311c8844da40de2c4a52e4dc6c2bda2ca3559e94b267f7f2
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.11.0]
|
4
|
+
|
5
|
+
- Add a global position that caused an error to the subscription's error JSON info. This will help you understand what event caused your subscription to fail.
|
6
|
+
- Improve long payloads in JSON preview in admin web UI in the way it does not moves content out of the visible area.
|
7
|
+
- Admin UI: adjust events filtering and displaying of stream context, stream name, stream id and event type when values of them contain empty strings or non-displayable characters
|
8
|
+
|
9
|
+
## [1.10.0]
|
10
|
+
- Admin UI: Adjust `SubscriptionSet` "Stop"/"Delete" buttons appearance. Now if `SubscriptionsSet` is not alive anymore(the related process is dead or does not exist anymore) - "Delete" button is shown. If `SubscriptionSet` is alive - "Stop" button is shown
|
11
|
+
- Admin IU: fixed several potential XSS vulnerabilities
|
12
|
+
- Admin IU: Add "Copy to clipboard" button near stream id that copies ruby stream definition
|
13
|
+
- Admin UI: allow deletion of streams with empty attribute values
|
14
|
+
|
3
15
|
## [1.9.0]
|
4
16
|
|
5
17
|
- Implement an ability to delete a stream
|
data/lib/pg_eventstore/errors.rb
CHANGED
@@ -243,4 +243,14 @@ module PgEventstore
|
|
243
243
|
"Too many records of #{stream.to_hash.inspect} stream to lock: #{number_of_records}"
|
244
244
|
end
|
245
245
|
end
|
246
|
+
|
247
|
+
class WrappedException < Error
|
248
|
+
attr_reader :original_exception
|
249
|
+
attr_reader :extra
|
250
|
+
|
251
|
+
def initialize(original_exception, extra)
|
252
|
+
@original_exception = original_exception
|
253
|
+
@extra = extra
|
254
|
+
end
|
255
|
+
end
|
246
256
|
end
|
@@ -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
|
@@ -6,6 +6,9 @@ module PgEventstore
|
|
6
6
|
include Extensions::UsingConnectionExtension
|
7
7
|
include Extensions::OptionsExtension
|
8
8
|
|
9
|
+
# @return [Time]
|
10
|
+
DEFAULT_TIMESTAMP = Time.at(0).utc.freeze
|
11
|
+
|
9
12
|
# @!attribute id
|
10
13
|
# @return [Integer, nil]
|
11
14
|
attribute(:id)
|
@@ -164,7 +167,7 @@ module PgEventstore
|
|
164
167
|
last_restarted_at: nil,
|
165
168
|
max_restarts_number: max_restarts_number,
|
166
169
|
chunk_query_interval: chunk_query_interval,
|
167
|
-
last_chunk_fed_at:
|
170
|
+
last_chunk_fed_at: DEFAULT_TIMESTAMP,
|
168
171
|
last_chunk_greatest_position: nil,
|
169
172
|
last_error: nil,
|
170
173
|
last_error_occurred_at: nil,
|
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]
|
@@ -101,6 +104,21 @@ module PgEventstore
|
|
101
104
|
end
|
102
105
|
rescue Errno::ENOENT
|
103
106
|
end
|
107
|
+
|
108
|
+
# @param exception [StandardError]
|
109
|
+
# @param extra [Hash] additional exception info
|
110
|
+
# @return [PgEventstore::WrappedException]
|
111
|
+
def wrap_exception(exception, **extra)
|
112
|
+
WrappedException.new(exception, extra)
|
113
|
+
end
|
114
|
+
|
115
|
+
# @param wrapped_exception [StandardError, PgEventstore::WrappedException]
|
116
|
+
# @return [StandardError]
|
117
|
+
def unwrap_exception(wrapped_exception)
|
118
|
+
return wrapped_exception.original_exception if wrapped_exception.is_a?(WrappedException)
|
119
|
+
|
120
|
+
wrapped_exception
|
121
|
+
end
|
104
122
|
end
|
105
123
|
end
|
106
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 }
|
@@ -69,8 +74,11 @@ module PgEventstore
|
|
69
74
|
# @param collection [PgEventstore::Web::Paginator::BaseCollection]
|
70
75
|
# @return [void]
|
71
76
|
def paginated_json_response(collection)
|
77
|
+
results = collection.collection.map do |attrs|
|
78
|
+
attrs.transform_values { escape_empty_string(_1) }
|
79
|
+
end
|
72
80
|
halt 200, {
|
73
|
-
results:
|
81
|
+
results: results,
|
74
82
|
pagination: { more: !collection.next_page_starting_id.nil?, starting_id: collection.next_page_starting_id }
|
75
83
|
}.to_json
|
76
84
|
end
|
@@ -108,9 +116,25 @@ module PgEventstore
|
|
108
116
|
COOKIES_FLASH_MESSAGE_KEY, { value: val, http_only: false, same_site: :lax, path: '/' }
|
109
117
|
)
|
110
118
|
end
|
119
|
+
|
120
|
+
# @param string [String, nil]
|
121
|
+
# @return [String, nil]
|
122
|
+
def escape_empty_string(string)
|
123
|
+
string == '' ? EMPTY_STRING_SIGN : string
|
124
|
+
end
|
125
|
+
|
126
|
+
# @param string [String, nil]
|
127
|
+
# @return [String, nil]
|
128
|
+
def unescape_empty_string(string)
|
129
|
+
string == EMPTY_STRING_SIGN ? '' : string
|
130
|
+
end
|
111
131
|
end
|
112
132
|
|
113
133
|
get '/' do
|
134
|
+
streams_filter = self.streams_filter&.map do |attrs|
|
135
|
+
attrs.transform_values { unescape_empty_string(_1) }
|
136
|
+
end
|
137
|
+
events_filter = self.events_filter&.map(&method(:unescape_empty_string))
|
114
138
|
@collection = Paginator::EventsCollection.new(
|
115
139
|
current_config,
|
116
140
|
starting_id: params[:starting_id]&.to_i,
|
@@ -169,7 +193,7 @@ module PgEventstore
|
|
169
193
|
get '/stream_contexts_filtering', provides: :json do
|
170
194
|
collection = Paginator::StreamContextsCollection.new(
|
171
195
|
current_config,
|
172
|
-
starting_id: params[:starting_id],
|
196
|
+
starting_id: unescape_empty_string(params[:starting_id]),
|
173
197
|
per_page: Paginator::StreamContextsCollection::PER_PAGE,
|
174
198
|
order: :asc,
|
175
199
|
options: { query: params[:term] }
|
@@ -180,10 +204,10 @@ module PgEventstore
|
|
180
204
|
get '/stream_names_filtering', provides: :json do
|
181
205
|
collection = Paginator::StreamNamesCollection.new(
|
182
206
|
current_config,
|
183
|
-
starting_id: params[:starting_id],
|
207
|
+
starting_id: unescape_empty_string(params[:starting_id]),
|
184
208
|
per_page: Paginator::StreamNamesCollection::PER_PAGE,
|
185
209
|
order: :asc,
|
186
|
-
options: { query: params[:term], context: params[:context] }
|
210
|
+
options: { query: params[:term], context: unescape_empty_string(params[:context]) }
|
187
211
|
)
|
188
212
|
paginated_json_response(collection)
|
189
213
|
end
|
@@ -191,10 +215,14 @@ module PgEventstore
|
|
191
215
|
get '/stream_ids_filtering', provides: :json do
|
192
216
|
collection = Paginator::StreamIdsCollection.new(
|
193
217
|
current_config,
|
194
|
-
starting_id: params[:starting_id],
|
218
|
+
starting_id: unescape_empty_string(params[:starting_id]),
|
195
219
|
per_page: Paginator::StreamIdsCollection::PER_PAGE,
|
196
220
|
order: :asc,
|
197
|
-
options: {
|
221
|
+
options: {
|
222
|
+
query: params[:term],
|
223
|
+
context: unescape_empty_string(params[:context]),
|
224
|
+
stream_name: unescape_empty_string(params[:stream_name])
|
225
|
+
}
|
198
226
|
)
|
199
227
|
paginated_json_response(collection)
|
200
228
|
end
|
@@ -202,7 +230,7 @@ module PgEventstore
|
|
202
230
|
get '/event_types_filtering', provides: :json do
|
203
231
|
collection = Paginator::EventTypesCollection.new(
|
204
232
|
current_config,
|
205
|
-
starting_id: params[:starting_id],
|
233
|
+
starting_id: unescape_empty_string(params[:starting_id]),
|
206
234
|
per_page: Paginator::EventTypesCollection::PER_PAGE,
|
207
235
|
order: :asc,
|
208
236
|
options: { query: params[:term] }
|
@@ -282,14 +310,35 @@ module PgEventstore
|
|
282
310
|
redirect(redirect_back_url(fallback_url: '/'))
|
283
311
|
end
|
284
312
|
|
285
|
-
post '/delete_stream
|
286
|
-
attrs =
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
message: "Stream #{stream.to_hash} has been successfully deleted.",
|
291
|
-
kind: 'success'
|
313
|
+
post '/delete_stream' do
|
314
|
+
attrs = {
|
315
|
+
context: params[:context]&.to_s,
|
316
|
+
stream_name: params[:stream_name]&.to_s,
|
317
|
+
stream_id: params[:stream_id]&.to_s,
|
292
318
|
}
|
319
|
+
|
320
|
+
err_message = ->(attrs) {
|
321
|
+
self.flash_message = {
|
322
|
+
message: "Could not delete #{attrs}. It is not valid stream for deletion.",
|
323
|
+
kind: 'error'
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
if attrs.values.none?(&:nil?)
|
328
|
+
stream = PgEventstore::Stream.new(**attrs)
|
329
|
+
if stream.system?
|
330
|
+
err_message.call(stream.to_hash)
|
331
|
+
else
|
332
|
+
PgEventstore.maintenance(current_config).delete_stream(stream)
|
333
|
+
self.flash_message = {
|
334
|
+
message: "Stream #{stream.to_hash} has been successfully deleted.",
|
335
|
+
kind: 'success'
|
336
|
+
}
|
337
|
+
end
|
338
|
+
else
|
339
|
+
err_message.call(attrs)
|
340
|
+
end
|
341
|
+
|
293
342
|
redirect(redirect_back_url(fallback_url: '/'))
|
294
343
|
end
|
295
344
|
end
|
@@ -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));
|
@@ -196,14 +233,14 @@ $(function(){
|
|
196
233
|
let $confirmationModal = $('#confirmation-modal');
|
197
234
|
|
198
235
|
$confirmationModal.on('hide.bs.modal', function(){
|
199
|
-
$(this).find('.modal-title').
|
200
|
-
$(this).find('.modal-body').
|
236
|
+
$(this).find('.modal-title').text('');
|
237
|
+
$(this).find('.modal-body').text('');
|
201
238
|
$(this).find('.confirm').off();
|
202
239
|
});
|
203
240
|
let showConfirmation = function(el, callback){
|
204
241
|
let $el = $(el);
|
205
|
-
$confirmationModal.find('.modal-body').
|
206
|
-
$confirmationModal.find('.modal-title').
|
242
|
+
$confirmationModal.find('.modal-body').text($el.data('confirm'));
|
243
|
+
$confirmationModal.find('.modal-title').text($el.data('confirm-title'));
|
207
244
|
$confirmationModal.modal('show');
|
208
245
|
$confirmationModal.one('click', '.confirm', callback);
|
209
246
|
}
|
@@ -323,3 +360,32 @@ $(function () {
|
|
323
360
|
$flashMessage.addClass(alertClass).removeClass('d-none');
|
324
361
|
Cookies.remove(window.flashMessageCookie);
|
325
362
|
});
|
363
|
+
|
364
|
+
// Copy to clipboard with a tooltip implementation
|
365
|
+
$(function () {
|
366
|
+
"use strict";
|
367
|
+
|
368
|
+
let $selector = $(".copy-to-clipboard");
|
369
|
+
|
370
|
+
$selector.tooltip({
|
371
|
+
container: "body",
|
372
|
+
title: function(){
|
373
|
+
let $this = $(this);
|
374
|
+
|
375
|
+
return $this.data("temp-title") || $this.data("title");
|
376
|
+
}
|
377
|
+
});
|
378
|
+
|
379
|
+
$selector.click(function () {
|
380
|
+
let $this = $(this);
|
381
|
+
|
382
|
+
navigator.clipboard.writeText($this.data("clipboard-content"));
|
383
|
+
|
384
|
+
$this.data("temp-title", "Copied!");
|
385
|
+
$this.data("bs.tooltip").hide();
|
386
|
+
$this.one("shown.bs.tooltip", function(){
|
387
|
+
$this.data("temp-title", null);
|
388
|
+
});
|
389
|
+
$this.data("bs.tooltip").show();
|
390
|
+
});
|
391
|
+
});
|
@@ -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
|
}
|
@@ -79,28 +79,41 @@ module PgEventstore
|
|
79
79
|
# @param updated_at [Time]
|
80
80
|
# @return [String] html status
|
81
81
|
def colored_state(state, interval, updated_at)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
<<~HTML
|
89
|
-
<span class="text-warning text-nowrap">
|
90
|
-
#{state}
|
91
|
-
<i class="fa fa-question-circle" data-toggle="tooltip" title="#{title}"></i>
|
92
|
-
</span>
|
93
|
-
HTML
|
82
|
+
text_class =
|
83
|
+
case state
|
84
|
+
when RunnerState::STATES[:running]
|
85
|
+
alive?(interval, updated_at) ? 'text-success' : 'text-warning'
|
86
|
+
when RunnerState::STATES[:dead]
|
87
|
+
'text-danger'
|
94
88
|
else
|
95
|
-
|
89
|
+
'text-info'
|
96
90
|
end
|
97
|
-
|
98
|
-
|
91
|
+
|
92
|
+
if alive?(interval, updated_at)
|
93
|
+
<<~HTML
|
94
|
+
<span class="#{text_class}">#{state}</span>
|
95
|
+
HTML
|
99
96
|
else
|
100
|
-
|
97
|
+
title = <<~TEXT
|
98
|
+
Something is wrong. Last update was more than #{interval} seconds ago(#{updated_at}).
|
99
|
+
TEXT
|
100
|
+
<<~HTML
|
101
|
+
<span class="#{text_class} text-nowrap">
|
102
|
+
#{state}
|
103
|
+
<i class="fa fa-question-circle" data-toggle="tooltip" title="#{title}"></i>
|
104
|
+
</span>
|
105
|
+
HTML
|
101
106
|
end
|
102
107
|
end
|
103
108
|
|
109
|
+
# @param interval [Integer]
|
110
|
+
# @param last_updated_at [Time]
|
111
|
+
# @return [Boolean]
|
112
|
+
def alive?(interval, last_updated_at)
|
113
|
+
# -1 is added as a margin to prevent false-positive result
|
114
|
+
last_updated_at > Time.now.utc - interval - 1
|
115
|
+
end
|
116
|
+
|
104
117
|
# @param ids [Array<Integer>]
|
105
118
|
# @return [String]
|
106
119
|
def delete_all_subscriptions_url(ids)
|
@@ -117,7 +130,8 @@ module PgEventstore
|
|
117
130
|
# @param stream_attrs [Hash]
|
118
131
|
# @return [String]
|
119
132
|
def delete_stream_url(stream_attrs)
|
120
|
-
|
133
|
+
encoded_params = Rack::Utils.build_nested_query(stream_attrs)
|
134
|
+
url("/delete_stream?#{encoded_params}")
|
121
135
|
end
|
122
136
|
end
|
123
137
|
end
|
@@ -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,11 +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>
|
7
|
-
<td><a href="<%= stream_path(event) %>"><%= event.stream.stream_id %></a></td>
|
5
|
+
<td><%= empty_characters_fallback(h event.stream.context) %></td>
|
6
|
+
<td><%= empty_characters_fallback(h event.stream.stream_name) %></td>
|
8
7
|
<td>
|
9
|
-
<
|
8
|
+
<a href="<%= stream_path(event) %>"><%= empty_characters_fallback(h event.stream.stream_id) %></a>
|
9
|
+
<a role="button" href="#" data-title="Copy stream definition." class="copy-to-clipboard"
|
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
|
+
<i class="fa fa-clipboard"></i>
|
12
|
+
</a>
|
13
|
+
</td>
|
14
|
+
<td>
|
15
|
+
<p class="float-left"><%= empty_characters_fallback(h event.type) %></p>
|
10
16
|
<% if event.link %>
|
11
17
|
<p class="float-left ml-2">
|
12
18
|
<i class="fa fa-link"></i>
|
@@ -26,7 +32,7 @@
|
|
26
32
|
</td>
|
27
33
|
</tr>
|
28
34
|
<tr class="event-payload d-none">
|
29
|
-
<td colspan="
|
35
|
+
<td colspan="9" class="json-cell">
|
30
36
|
<strong>Data:</strong>
|
31
37
|
<pre><%= h JSON.pretty_generate(event.data) %></pre>
|
32
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>
|
@@ -85,14 +85,16 @@
|
|
85
85
|
Restore
|
86
86
|
</a>
|
87
87
|
<% end %>
|
88
|
-
<% if PgEventstore::RunnerState::STATES.values_at(:running, :dead).include?(subscriptions_set.state) %>
|
88
|
+
<% if PgEventstore::RunnerState::STATES.values_at(:running, :dead).include?(subscriptions_set.state) && alive?(PgEventstore::SubscriptionsSetLifecycle::HEARTBEAT_INTERVAL, subscriptions_set.updated_at) %>
|
89
89
|
<a class="btn btn-warning" data-confirm="You are about to stop SubscriptionsSet#<%= subscriptions_set.id %>. This will also delete it and will result in stopping all related Subscriptions. If you used pg_eventstore CLI to start subscriptions - the related process will also be stopped. Continue?" data-confirm-title="Stop SubscriptionsSet" data-method="post" href="<%= subscriptions_set_cmd_url(subscriptions_set.id, subscriptions_set_cmd('Stop')) %>" data-toggle="tooltip" title="This action will delete Subscriptions Set and release all related Subscriptions.">
|
90
90
|
Stop
|
91
91
|
</a>
|
92
92
|
<% end %>
|
93
|
-
|
94
|
-
Delete
|
95
|
-
|
93
|
+
<% unless alive?(PgEventstore::SubscriptionsSetLifecycle::HEARTBEAT_INTERVAL, subscriptions_set.updated_at) %>
|
94
|
+
<a class="btn btn-danger" data-confirm="You are about to delete SubscriptionsSet#<%= subscriptions_set.id %>. Continue?" data-confirm-title="Delete SubscriptionsSet" data-method="post" href="<%= url("/delete_subscriptions_set/#{subscriptions_set.id}") %>" data-toggle="tooltip" title="Use this action only on stuck Subscriptions Set - to clean it up.">
|
95
|
+
Delete
|
96
|
+
</a>
|
97
|
+
<% end %>
|
96
98
|
</td>
|
97
99
|
</tr>
|
98
100
|
<% if subscriptions_set.last_error %>
|
@@ -160,7 +162,7 @@
|
|
160
162
|
</td>
|
161
163
|
<td><%= subscription.current_position %></td>
|
162
164
|
<td><%= subscription.chunk_query_interval %>s</td>
|
163
|
-
<td><%= subscription.last_chunk_fed_at %></td>
|
165
|
+
<td><%= subscription.last_chunk_fed_at if subscription.last_chunk_fed_at > PgEventstore::Subscription::DEFAULT_TIMESTAMP %></td>
|
164
166
|
<td><%= colored_state(subscription.state, PgEventstore::SubscriptionsLifecycle::HEARTBEAT_INTERVAL, subscription.updated_at) %></td>
|
165
167
|
<td>
|
166
168
|
<% if subscription.average_event_processing_time %>
|
@@ -208,13 +210,13 @@
|
|
208
210
|
</td>
|
209
211
|
</tr>
|
210
212
|
<tr class="collapse" id="options-<%= subscription.id %>">
|
211
|
-
<td colspan="16">
|
213
|
+
<td colspan="16" class="json-cell">
|
212
214
|
<pre><%= h JSON.pretty_generate(subscription.options) %></pre>
|
213
215
|
</td>
|
214
216
|
</tr>
|
215
217
|
<% if subscription.last_error %>
|
216
218
|
<tr class="collapse" id="last-error-<%= subscription.id %>">
|
217
|
-
<td colspan="16">
|
219
|
+
<td colspan="16" class="json-cell">
|
218
220
|
<pre><%= h JSON.pretty_generate(subscription.last_error) %></pre>
|
219
221
|
</td>
|
220
222
|
</tr>
|
@@ -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
|
@@ -4,11 +4,11 @@ module PgEventstore
|
|
4
4
|
|
5
5
|
def self.update_subscription_stats: (PgEventstore::Subscription subscription, PgEventstore::SubscriptionHandlerPerformance stats, Integer current_position) -> void
|
6
6
|
|
7
|
-
def self.update_subscription_error: (PgEventstore::Subscription subscription,
|
7
|
+
def self.update_subscription_error: (PgEventstore::Subscription subscription, PgEventstore::WrappedException error) -> void
|
8
8
|
|
9
9
|
def self.restart_events_processor: (PgEventstore::Subscription subscription, _RestartTerminator? restart_terminator,
|
10
10
|
_FailedSubscriptionNotifier? failed_subscription_notifier, PgEventstore::EventsProcessor events_processor,
|
11
|
-
|
11
|
+
PgEventstore::WrappedException error) -> void
|
12
12
|
|
13
13
|
def self.update_subscription_chunk_stats: (PgEventstore::Subscription subscription, Integer global_position) -> void
|
14
14
|
|
data/sig/pg_eventstore/utils.rbs
CHANGED
@@ -20,5 +20,9 @@ module PgEventstore
|
|
20
20
|
def self.underscore_str: (String str) -> String
|
21
21
|
|
22
22
|
def self.original_global_position: (Hash[untyped, untyped] raw_event) -> Integer
|
23
|
+
|
24
|
+
def self.unwrap_exception: (PgEventstore::WrappedException | StandardError wrapped_exception)-> StandardError
|
25
|
+
|
26
|
+
def self.wrap_exception: (StandardError exception, **untyped extra)-> PgEventstore::WrappedException
|
23
27
|
end
|
24
28
|
end
|
@@ -5,6 +5,8 @@ module PgEventstore
|
|
5
5
|
COOKIES_FLASH_MESSAGE_KEY: String
|
6
6
|
DEFAULT_ADMIN_UI_CONFIG: Symbol
|
7
7
|
|
8
|
+
EMPTY_STRING_SIGN: String
|
9
|
+
|
8
10
|
def asset_url: (String path) -> String
|
9
11
|
|
10
12
|
def connection: -> PgEventstore::Connection
|
@@ -13,6 +15,8 @@ module PgEventstore
|
|
13
15
|
|
14
16
|
def current_config=: (untyped val) -> void
|
15
17
|
|
18
|
+
def escape_empty_string: (String? string) -> String?
|
19
|
+
|
16
20
|
def events_filter: -> Array[String]?
|
17
21
|
|
18
22
|
def flash_message=: (({ message: String, kind: String }) val)-> String
|
@@ -30,6 +34,8 @@ module PgEventstore
|
|
30
34
|
def streams_filter: -> Array[Hash[untyped, untyped]]?
|
31
35
|
|
32
36
|
def system_stream: -> String?
|
37
|
+
|
38
|
+
def unescape_empty_string: (String? string) -> String?
|
33
39
|
end
|
34
40
|
end
|
35
41
|
end
|
@@ -2,6 +2,8 @@ module PgEventstore
|
|
2
2
|
module Web
|
3
3
|
module Subscriptions
|
4
4
|
module Helpers
|
5
|
+
def alive?: (Integer interval, Time last_updated_at)-> bool
|
6
|
+
|
5
7
|
def delete_event_url: (Integer global_position)-> String
|
6
8
|
|
7
9
|
def delete_stream_url: (({ context: String, stream_name: String, stream_id: String }) stream_attrs)-> String
|
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.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Dzyzenko
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -368,7 +368,7 @@ metadata:
|
|
368
368
|
homepage_uri: https://github.com/yousty/pg_eventstore
|
369
369
|
source_code_uri: https://github.com/yousty/pg_eventstore
|
370
370
|
changelog_uri: https://github.com/yousty/pg_eventstore/blob/main/CHANGELOG.md
|
371
|
-
post_install_message:
|
371
|
+
post_install_message:
|
372
372
|
rdoc_options: []
|
373
373
|
require_paths:
|
374
374
|
- lib
|
@@ -383,8 +383,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
383
383
|
- !ruby/object:Gem::Version
|
384
384
|
version: '0'
|
385
385
|
requirements: []
|
386
|
-
rubygems_version: 3.5.
|
387
|
-
signing_key:
|
386
|
+
rubygems_version: 3.5.22
|
387
|
+
signing_key:
|
388
388
|
specification_version: 4
|
389
389
|
summary: EventStore implementation using PostgreSQL
|
390
390
|
test_files: []
|