pg_eventstore 1.10.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 +6 -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/utils.rb +22 -4
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +35 -7
- 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/sig/pg_eventstore/errors.rbs +8 -0
- 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 +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,11 @@ | |
| 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 | 
            +
             | 
| 3 9 | 
             
            ## [1.10.0]
         | 
| 4 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
         | 
| 5 11 | 
             
            - Admin IU: fixed several potential XSS vulnerabilities
         | 
    
        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
         | 
    
        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] }
         | 
| @@ -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>
         | 
| @@ -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
         | 
    
        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: []
         |