pg_eventstore 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +1 -0
  4. data/docs/configuration.md +3 -0
  5. data/docs/maintenance.md +51 -0
  6. data/lib/pg_eventstore/client.rb +0 -2
  7. data/lib/pg_eventstore/commands/delete_event.rb +38 -0
  8. data/lib/pg_eventstore/commands/delete_stream.rb +18 -0
  9. data/lib/pg_eventstore/commands.rb +2 -0
  10. data/lib/pg_eventstore/errors.rb +19 -1
  11. data/lib/pg_eventstore/maintenance.rb +48 -0
  12. data/lib/pg_eventstore/queries/maintenance_queries.rb +81 -0
  13. data/lib/pg_eventstore/queries.rb +5 -0
  14. data/lib/pg_eventstore/version.rb +1 -1
  15. data/lib/pg_eventstore/web/application.rb +49 -0
  16. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +66 -0
  17. data/lib/pg_eventstore/web/public/javascripts/vendor/js.cookie.min.js +2 -0
  18. data/lib/pg_eventstore/web/subscriptions/helpers.rb +12 -0
  19. data/lib/pg_eventstore/web/views/home/dashboard.erb +32 -0
  20. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +1 -1
  21. data/lib/pg_eventstore/web/views/home/partials/events.erb +8 -2
  22. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +11 -6
  23. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +1 -1
  24. data/lib/pg_eventstore/web/views/layouts/application.erb +12 -0
  25. data/lib/pg_eventstore.rb +9 -0
  26. data/sig/pg_eventstore/commands/delete_event.rbs +13 -0
  27. data/sig/pg_eventstore/commands/delete_stream.rbs +7 -0
  28. data/sig/pg_eventstore/errors.rbs +10 -0
  29. data/sig/pg_eventstore/maintenance.rbs +19 -0
  30. data/sig/pg_eventstore/queries/maintenance_queries.rbs +21 -0
  31. data/sig/pg_eventstore/queries.rbs +8 -6
  32. data/sig/pg_eventstore/web/application.rbs +3 -0
  33. data/sig/pg_eventstore/web/subscriptions/helpers.rbs +4 -0
  34. data/sig/pg_eventstore.rbs +2 -0
  35. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac379ed7a4c1c8ca46edd8e4ec537dafd575dd50749a140d15c49c24a1ece1c1
4
- data.tar.gz: c5f83c5aa8fccc6f7100172270c951fda96ca7926c7a1b6b37f90217024e3e09
3
+ metadata.gz: 7c0936a5580f6340a43f65d5b1b3362855b30e78fb25bdd498bf21c759458fcb
4
+ data.tar.gz: d18a05c7c6917e66c83de05013afcf6b9f25aaf7027e32fb1345553f5398a120
5
5
  SHA512:
6
- metadata.gz: 6ffbe0ddbfff6207069a527c7f41693e59d00a3217f68697c851fb2582300fefc569be8035f30924a225b95522ee5ef29ec26c9c6134fe705b31f6b32d0c39a3
7
- data.tar.gz: 3b1860f5f351f5ffcd56f09c557661796a98f46a49c9a9bf8945667074ad8705f7cb9998639031fe07a4a2dfb89a0bf636b1e8157a21374da869cf4e76377792
6
+ metadata.gz: b11fcbc08f2853a69c288dcd08498a5e40277cdeaf32ccba71ade48eac03cf3fd0c157abe0058daedb27aca945406afa8f3078f149e045ff63d89a47ca12da36
7
+ data.tar.gz: c021e192cfd8f61364235fa7ccdd2d97a37c39fe304f9167fe7cda508acd7c65b84bc2d168687a1cec185cf07870183b70fc849508a434ea13935398687cbea4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.0]
4
+
5
+ - Implement an ability to delete a stream
6
+ - Implement an ability to delete an event
7
+ - Add "Delete event" and "Delete stream" buttons into admin UI
8
+
3
9
  ## [1.8.0]
4
10
  - Introduce default config for admin web UI. Now if you define `:admin_web_ui` config - it will be preferred over default config
5
11
  - Fix pagination of events in admin UI
data/README.md CHANGED
@@ -45,6 +45,7 @@ Documentation chapters:
45
45
  - [Linking events](docs/linking_events.md)
46
46
  - [Reading events](docs/reading_events.md)
47
47
  - [Subscriptions](docs/subscriptions.md)
48
+ - [Maintenance functions](docs/maintenance.md)
48
49
  - [Writing middlewares](docs/writing_middleware.md)
49
50
  - [How to make multiple commands atomic](docs/multiple_commands.md)
50
51
  - [Admin UI](docs/admin_ui.md)
@@ -45,6 +45,9 @@ Tell `PgEventstore` which config you want to use:
45
45
  PgEventstore.client(:pg_db_1).read(PgEventstore::Stream.all_stream)
46
46
  # Read from "all" stream using :pg_db_2 config
47
47
  PgEventstore.client(:pg_db_2).read(PgEventstore::Stream.all_stream)
48
+ # Delete a stream using :pg_db_1 config
49
+ stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyStream', stream_id: '1')
50
+ PgEventstore.maintenance.delete_stream(stream)
48
51
  ```
49
52
 
50
53
  ### Default config
@@ -0,0 +1,51 @@
1
+ # Maintenance
2
+
3
+ `pg_eventstore` provides maintenance functional which can be used to clean up various objects in your database.
4
+
5
+ ## Delete a stream
6
+
7
+ ```ruby
8
+ stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyStream', stream_id: '1')
9
+ PgEventstore.client.append_to_stream(stream, PgEventstore::Event.new)
10
+ PgEventstore.maintenance.delete_stream(stream) # => true
11
+ ```
12
+ deletes all events in the given stream. If the given stream does not exist - `false` is returned:
13
+
14
+ ```ruby
15
+ stream = PgEventstore::Stream.new(context: 'NonExistingCtx', stream_name: 'NonExistingStream', stream_id: '1')
16
+ PgEventstore.maintenance.delete_stream(stream) # => false
17
+ ```
18
+
19
+ **Please note that this operation does not automatically delete links to deleted events - you have to do delete them separately.**
20
+
21
+ ## Delete an event
22
+
23
+ ```ruby
24
+ stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyStream', stream_id: '1')
25
+ PgEventstore.client.append_to_stream(stream, PgEventstore::Event.new)
26
+ # Grab the first event for further deletion
27
+ event = PgEventstore.client.read(stream, options: { max_count: 1 }).first
28
+ PgEventstore.maintenance.delete_event(event) # => true
29
+ ```
30
+ deletes the given event. If the given event does not exist - `false` is returned:
31
+
32
+ ```ruby
33
+ stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyStream', stream_id: '1')
34
+ PgEventstore.client.append_to_stream(stream, PgEventstore::Event.new)
35
+ # Grab the first event for further deletion
36
+ event = PgEventstore.client.read(stream, options: { max_count: 1 }).first
37
+ PgEventstore.maintenance.delete_event(event) # => true
38
+ PgEventstore.maintenance.delete_event(event) # => false
39
+ ```
40
+
41
+ **Please note that this operation does not automatically delete links to the deleted event - you have to do delete them separately.**
42
+
43
+ ### Deleting an event in a large stream
44
+
45
+ There can be a situation when you would like to delete an event in a large stream. If an event is in the position where there are 1000+ events after it in a stream - a `PgEventstore::TooManyRecordsToLockError` error will be raised. This is because in addition to removing the event, we need to adjust the rest of the stream by updating the `#stream_revision` of all events that come after this event. To overcome this limitation - you can provide `force: true` flag:
46
+
47
+ ```ruby
48
+ PgEventstore.maintenance.delete_event(event, force: true)
49
+ ```
50
+
51
+ Because the update operation can lock thousands of events - `#delete_event` can be slow and can slow down other processes which insert events in the same stream at the same moment. **Thus, it is recommended to avoid using `#delete_event` in your business logic.**
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'commands'
4
3
  require_relative 'event_serializer'
5
4
  require_relative 'event_deserializer'
6
- require_relative 'queries'
7
5
 
8
6
  module PgEventstore
9
7
  class Client
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class DeleteEvent < AbstractCommand
7
+ # Determines max allowed number of records to lock during an update. If the threshold is reached - an error is
8
+ # raised.
9
+ # @return [Integer]
10
+ MAX_RECORDS_TO_LOCK = 1_000
11
+
12
+ # @param event [PgEventstore::Event]
13
+ # @param force [Boolean]
14
+ # @return [Boolean]
15
+ def call(event, force:)
16
+ queries.transactions.transaction do
17
+ event = queries.maintenance.reload_event(event)
18
+ next false unless event
19
+
20
+ check_records_number_to_lock(event) unless force
21
+ queries.maintenance.delete_event(event)
22
+ queries.maintenance.adjust_stream_revisions(event.stream, event.stream_revision)
23
+ true
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # @param event [PgEventstore::Event]
30
+ # @return [void]
31
+ # @raise [PgEventstore::TooManyRecordsToLockError]
32
+ def check_records_number_to_lock(event)
33
+ records_to_lock = queries.maintenance.events_to_lock_count(event.stream, event.stream_revision)
34
+ raise TooManyRecordsToLockError.new(event.stream, records_to_lock) if records_to_lock > MAX_RECORDS_TO_LOCK
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class DeleteStream < AbstractCommand
7
+ # @param stream [PgEventstore::Stream]
8
+ # @return [Boolean]
9
+ def call(stream)
10
+ raise SystemStreamError, stream if stream.system?
11
+
12
+ queries.transactions.transaction do
13
+ queries.maintenance.delete_stream(stream) > 0
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -9,3 +9,5 @@ require_relative 'commands/regular_stream_read_paginated'
9
9
  require_relative 'commands/system_stream_read_paginated'
10
10
  require_relative 'commands/multiple'
11
11
  require_relative 'commands/link_to'
12
+ require_relative 'commands/delete_stream'
13
+ require_relative 'commands/delete_event'
@@ -41,7 +41,7 @@ module PgEventstore
41
41
  # @param stream [PgEventstore::Stream]
42
42
  def initialize(stream)
43
43
  @stream = stream
44
- super("Stream #{stream.inspect} is a system stream and can't be used to append events.")
44
+ super("Can't perform this action with #{stream.inspect} system stream.")
45
45
  end
46
46
  end
47
47
 
@@ -225,4 +225,22 @@ module PgEventstore
225
225
 
226
226
  class EmptyChunkFedError < Error
227
227
  end
228
+
229
+ class TooManyRecordsToLockError < Error
230
+ attr_reader :stream
231
+ attr_reader :number_of_records
232
+
233
+ # @param stream [PgEventstore::Stream]
234
+ # @param number_of_records [Integer]
235
+ def initialize(stream, number_of_records)
236
+ @stream = stream
237
+ @number_of_records = number_of_records
238
+ super(user_friendly_message)
239
+ end
240
+
241
+ # @return [String]
242
+ def user_friendly_message
243
+ "Too many records of #{stream.to_hash.inspect} stream to lock: #{number_of_records}"
244
+ end
245
+ end
228
246
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ class Maintenance
5
+ # @!attribute config
6
+ # @return [PgEventstore::Config]
7
+ attr_reader :config
8
+ private :config
9
+
10
+ # @param config [PgEventstore::Config]
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ # @param stream [PgEventstore::Stream]
16
+ # @return [Boolean] whether a stream was deleted successfully
17
+ def delete_stream(stream)
18
+ Commands::DeleteStream.new(
19
+ Queries.new(transactions: transaction_queries, maintenance: maintenance_queries)
20
+ ).call(stream)
21
+ end
22
+
23
+ # @param event [PgEventstore::Event] persisted event
24
+ # @return [Boolean] whether an event was deleted successfully
25
+ def delete_event(event, force: false)
26
+ Commands::DeleteEvent.new(
27
+ Queries.new(transactions: transaction_queries, maintenance: maintenance_queries)
28
+ ).call(event, force: force)
29
+ end
30
+
31
+ private
32
+
33
+ # @return [PgEventstore::MaintenanceQueries]
34
+ def maintenance_queries
35
+ MaintenanceQueries.new(connection)
36
+ end
37
+
38
+ # @return [PgEventstore::TransactionQueries]
39
+ def transaction_queries
40
+ TransactionQueries.new(connection)
41
+ end
42
+
43
+ # @return [PgEventstore::Connection]
44
+ def connection
45
+ PgEventstore.connection(config.name)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class MaintenanceQueries
6
+ # @!attribute connection
7
+ # @return [PgEventstore::Connection]
8
+ attr_reader :connection
9
+ private :connection
10
+
11
+ # @param connection [PgEventstore::Connection]
12
+ def initialize(connection)
13
+ @connection = connection
14
+ end
15
+
16
+ # @param stream [PgEventstore::Stream]
17
+ # @return [Integer] number of deleted events of the given stream
18
+ def delete_stream(stream)
19
+ connection.with do |conn|
20
+ conn.exec_params(<<~SQL, stream.deconstruct)
21
+ DELETE FROM events WHERE context = $1 AND stream_name = $2 AND stream_id = $3
22
+ SQL
23
+ end.cmd_tuples
24
+ end
25
+
26
+ # @param event [PgEventstore::Event]
27
+ # @return [Integer] number of deleted events
28
+ def delete_event(event)
29
+ connection.with do |conn|
30
+ conn.exec_params(<<~SQL, [event.stream.context, event.stream.stream_name, event.type, event.global_position])
31
+ DELETE FROM events WHERE context = $1 AND stream_name = $2 AND type = $3 AND global_position = $4
32
+ SQL
33
+ end.cmd_tuples
34
+ end
35
+
36
+ # @param stream [PgEventstore::Stream]
37
+ # @param after_revision [Integer]
38
+ # @return [void]
39
+ def adjust_stream_revisions(stream, after_revision)
40
+ connection.with do |conn|
41
+ conn.exec_params(<<~SQL, [stream.context, stream.stream_name, stream.stream_id, after_revision])
42
+ UPDATE events SET stream_revision = stream_revision - 1
43
+ WHERE context = $1 AND stream_name = $2
44
+ AND stream_id = $3 AND stream_revision > $4
45
+ SQL
46
+ end
47
+ end
48
+
49
+ # @param stream [PgEventstore::Stream]
50
+ # @param after_revision [Integer]
51
+ # @return [Integer]
52
+ def events_to_lock_count(stream, after_revision)
53
+ connection.with do |conn|
54
+ conn.exec_params(<<~SQL, [*stream.deconstruct, after_revision])
55
+ EXPLAIN SELECT * FROM events
56
+ WHERE context = $1 AND stream_name = $2 AND stream_id = $3 AND stream_revision > $4
57
+ SQL
58
+ end.to_a.first['QUERY PLAN'].match(/rows=(\d+)/)[1].to_i
59
+ end
60
+
61
+ # @param event [PgEventstore::Event]
62
+ # @return [PgEventstore::Event]
63
+ def reload_event(event)
64
+ event_attrs = connection.with do |conn|
65
+ conn.exec_params(<<~SQL, [event.stream&.context, event.stream&.stream_name, event.type, event.global_position])
66
+ SELECT * FROM events WHERE context = $1 AND stream_name = $2 AND type = $3 AND global_position = $4 LIMIT 1
67
+ SQL
68
+ end.to_a.first
69
+ return unless event_attrs
70
+
71
+ basic_deserializer.deserialize(event_attrs)
72
+ end
73
+
74
+ private
75
+
76
+ # @return [PgEventstore::EventDeserializer]
77
+ def basic_deserializer
78
+ EventDeserializer.new([], ->(_event_type) { Event })
79
+ end
80
+ end
81
+ end
@@ -6,6 +6,7 @@ require_relative 'queries/transaction_queries'
6
6
  require_relative 'queries/event_queries'
7
7
  require_relative 'queries/partition_queries'
8
8
  require_relative 'queries/links_resolver'
9
+ require_relative 'queries/maintenance_queries'
9
10
 
10
11
  module PgEventstore
11
12
  # @!visibility private
@@ -21,5 +22,9 @@ module PgEventstore
21
22
  # @!attribute transactions
22
23
  # @return [PgEventstore::TransactionQueries, nil]
23
24
  attribute(:transactions)
25
+
26
+ # @!attribute maintenance
27
+ # @return [PgEventstore::MaintenanceQueries, nil]
28
+ attribute(:maintenance)
24
29
  end
25
30
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @return [String]
5
- VERSION = "1.8.0"
5
+ VERSION = "1.9.0"
6
6
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'base64'
4
5
 
5
6
  module PgEventstore
6
7
  module Web
@@ -9,6 +10,8 @@ module PgEventstore
9
10
  DEFAULT_ADMIN_UI_CONFIG = :admin_web_ui
10
11
  # @return [String]
11
12
  COOKIES_CONFIG_KEY = 'current_config'
13
+ # @return [String]
14
+ COOKIES_FLASH_MESSAGE_KEY = 'flash_message'
12
15
 
13
16
  set :static_cache_control, [:private, max_age: 86400]
14
17
  set :environment, -> { (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['APP_ENV'])&.to_sym || :development }
@@ -97,6 +100,14 @@ module PgEventstore
97
100
  def resolve_link_tos?
98
101
  params.key?(:resolve_link_tos) ? params[:resolve_link_tos] == 'true' : true
99
102
  end
103
+
104
+ # @param val [Hash]
105
+ def flash_message=(val)
106
+ val = Base64.urlsafe_encode64(val.to_json)
107
+ response.set_cookie(
108
+ COOKIES_FLASH_MESSAGE_KEY, { value: val, http_only: false, same_site: :lax, path: '/' }
109
+ )
110
+ end
100
111
  end
101
112
 
102
113
  get '/' do
@@ -243,6 +254,44 @@ module PgEventstore
243
254
 
244
255
  redirect redirect_back_url(fallback_url: url('/subscriptions'))
245
256
  end
257
+
258
+ post '/delete_event/:global_position' do
259
+ params in { data: { force: String => force } }
260
+ global_position = params[:global_position].to_i
261
+ force = force == 'true'
262
+ event = PgEventstore.client(current_config).read(
263
+ PgEventstore::Stream.all_stream, options: { max_count: 1, from_position: global_position }
264
+ ).first
265
+ if event&.global_position == global_position
266
+ begin
267
+ PgEventstore.maintenance(current_config).delete_event(event, force: force)
268
+ self.flash_message = {
269
+ message: "An event at global position #{event.global_position} has been deleted successfully.",
270
+ kind: 'success'
271
+ }
272
+ rescue TooManyRecordsToLockError => e
273
+ text = <<~TEXT
274
+ Could not delete an event at global position #{event.global_position} - too many \
275
+ records(~#{e.number_of_records}) to lock.
276
+ TEXT
277
+ self.flash_message = { message: text, kind: 'error' }
278
+ end
279
+ else
280
+ self.flash_message = { message: 'Failed to delete an event - event does not exist.', kind: 'warning' }
281
+ end
282
+ redirect(redirect_back_url(fallback_url: '/'))
283
+ end
284
+
285
+ post '/delete_stream/:context/:stream_name/:stream_id' do
286
+ attrs = Hash[params.slice(:context, :stream_name, :stream_id)].transform_keys(&:to_sym)
287
+ stream = PgEventstore::Stream.new(**attrs)
288
+ PgEventstore.maintenance(current_config).delete_stream(stream)
289
+ self.flash_message = {
290
+ message: "Stream #{stream.to_hash} has been successfully deleted.",
291
+ kind: 'success'
292
+ }
293
+ redirect(redirect_back_url(fallback_url: '/'))
294
+ end
246
295
  end
247
296
  end
248
297
  end
@@ -5,6 +5,11 @@ $(function(){
5
5
  let $contextSelect = $filter.find('select[name*="context"]');
6
6
  let $streamNameSelect = $filter.find('select[name*="stream_name"]');
7
7
  let $streamIdSelect = $filter.find('select[name*="stream_id"]');
8
+
9
+ let removeDeleteBtn = function(){
10
+ $filter.find('.delete-stream').remove();
11
+ }
12
+
8
13
  $contextSelect.select2({
9
14
  ajax: {
10
15
  url: $contextSelect.data('url'),
@@ -58,6 +63,9 @@ $(function(){
58
63
  },
59
64
  allowClear: true
60
65
  });
66
+ $contextSelect.on('change.select2', removeDeleteBtn);
67
+ $streamNameSelect.on('change.select2', removeDeleteBtn);
68
+ $streamIdSelect.on('change.select2', removeDeleteBtn);
61
69
  }
62
70
 
63
71
  let initSystemStreamFilterAutocomplete = function($filter){
@@ -257,3 +265,61 @@ $(function(){
257
265
  window.location.href = $selected.data('url');
258
266
  });
259
267
  });
268
+
269
+ // Event deletion handling
270
+ $(function(){
271
+ "use strict";
272
+
273
+ let $deleteEventModal = $('#delete-event-modal');
274
+
275
+ $deleteEventModal.on('hide.bs.modal', function(){
276
+ $(this).find('.global-position-text').html('');
277
+ $(this).find('form').removeAttr('action');
278
+ });
279
+
280
+ $deleteEventModal.on('show.bs.modal', function(e){
281
+ let $clickedLink = $(e.relatedTarget);
282
+ $(this).find('.global-position-text').html($clickedLink.data('global-position'));
283
+ $(this).find('form').attr('action', $clickedLink.data('url'));
284
+ });
285
+ });
286
+
287
+ // Flash messages
288
+ $(function () {
289
+ "use strict";
290
+
291
+ let message = Cookies.get(window.flashMessageCookie);
292
+ if (!message)
293
+ return;
294
+
295
+ try {
296
+ message = Uint8Array.fromBase64(message, { alphabet: "base64url" });
297
+ message = new TextDecoder().decode(message);
298
+ message = JSON.parse(message);
299
+ } catch (e) {
300
+ console.debug(message);
301
+ console.debug(e);
302
+ Cookies.remove(window.flashMessageCookie);
303
+ return;
304
+ }
305
+
306
+ let $flashMessage = $('#flash-message');
307
+ let alertClass;
308
+ switch(message.kind) {
309
+ case "error":
310
+ alertClass = "alert-danger";
311
+ break;
312
+ case "warning":
313
+ alertClass = "alert-warning";
314
+ break;
315
+ case "success":
316
+ alertClass = "alert-success";
317
+ break;
318
+ default:
319
+ alertClass = "alert-light";
320
+ }
321
+
322
+ $flashMessage.find('.message').text(message.message);
323
+ $flashMessage.addClass(alertClass).removeClass('d-none');
324
+ Cookies.remove(window.flashMessageCookie);
325
+ });
@@ -0,0 +1,2 @@
1
+ /*! js-cookie v3.0.5 | MIT */
2
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}var t=function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"});return t}));
@@ -107,6 +107,18 @@ module PgEventstore
107
107
  encoded_params = Rack::Utils.build_nested_query(ids: ids)
108
108
  url("/delete_all_subscriptions?#{encoded_params}")
109
109
  end
110
+
111
+ # @param global_position [Integer]
112
+ # @return [String]
113
+ def delete_event_url(global_position)
114
+ url("/delete_event/#{global_position}")
115
+ end
116
+
117
+ # @param stream_attrs [Hash]
118
+ # @return [String]
119
+ def delete_stream_url(stream_attrs)
120
+ url("/delete_stream/#{stream_attrs[:context]}/#{stream_attrs[:stream_name]}/#{stream_attrs[:stream_id]}")
121
+ end
110
122
  end
111
123
  end
112
124
  end
@@ -165,3 +165,35 @@
165
165
  </div>
166
166
  </div>
167
167
  </div>
168
+
169
+ <div class="modal fade" id="delete-event-modal" tabindex="-1" role="dialog" aria-labelledby="delete-event-modal" aria-hidden="true">
170
+ <div class="modal-dialog modal-dialog-centered" role="document">
171
+ <div class="modal-content">
172
+ <div class="modal-header">
173
+ <h5 class="modal-title">Delete Event</h5>
174
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
175
+ <span aria-hidden="true">&times;</span>
176
+ </button>
177
+ </div>
178
+ <div class="modal-body font-weight-bold text-break">
179
+ <h5>
180
+ You are about to delete Event on global position <span class="global-position-text"></span>. This action is irreversible. Continue?
181
+ </h5>
182
+
183
+ <form id="delete-event-form" data-parsley-validate="" class="form-horizontal form-label-left" novalidate="" method="POST">
184
+ <input type="hidden" name="data[force]" value="false">
185
+ <div class="checkbox">
186
+ <label>
187
+ <input type="checkbox" id="force-delete" name="data[force]" autocomplete="off" value="true">
188
+ Ignore limitations(see <a class="text-info" href="https://github.com/yousty/pg_eventstore/blob/main/docs/maintenance.md#deleting-an-event-in-a-large-stream" target="_blank" rel="noreferrer,nofollow">docs</a>).
189
+ </label>
190
+ </div>
191
+ </form>
192
+ </div>
193
+ <div class="modal-footer">
194
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
195
+ <button type="submit" class="btn btn-danger" form="delete-event-form">Delete</button>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
@@ -1,6 +1,6 @@
1
1
  <div class="form-row align-items-center">
2
2
  <div class="col-11">
3
- <select name="filter[events][]" class="form-control mb-2" data-placeholder="Event type" data-url="<%= url('/event_types_filtering') %>">
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
  <option value="<%= h event_type %>" selected><%= h event_type %></option>
@@ -16,8 +16,14 @@
16
16
  </td>
17
17
  <td><%= event.created_at.strftime('%F %T') %></td>
18
18
  <td><%= event.id %></td>
19
- <td><a href="javascript: void(0);" class="d-inline-block text-nowrap toggle-event-data">JSON <i
20
- class="fa fa-eye"></i> </a></td>
19
+ <td>
20
+ <a href="javascript: void(0);" class="d-inline-block text-nowrap toggle-event-data">
21
+ JSON <i class="fa fa-eye"></i>
22
+ </a>
23
+ <a href="javascript: void(0);" class="ml-2 btn btn-danger btn-small delete-event-btn" data-global-position="<%= event.global_position %>" data-url="<%= delete_event_url(event.global_position) %>" data-toggle="modal" data-target="#delete-event-modal">
24
+ Delete
25
+ </a>
26
+ </td>
21
27
  </tr>
22
28
  <tr class="event-payload d-none">
23
29
  <td colspan="8">
@@ -1,31 +1,36 @@
1
1
  <div class="form-row align-items-center">
2
2
  <div class="col-3">
3
- <select name="filter[streams][][context]" class="form-control mb-2" data-placeholder="Context" data-url="<%= url('/stream_contexts_filtering') %>">
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
  <option value="<%= h stream[:context] %>" selected><%= h stream[:context] %></option>
7
7
  <% end %>
8
8
  </select>
9
9
  </div>
10
- <div class="col-4">
11
- <select name="filter[streams][][stream_name]" class="form-control mb-2" data-placeholder="Stream name" data-url="<%= url('/stream_names_filtering') %>">
10
+ <div class="col-3">
11
+ <select name="filter[streams][][stream_name]" class="form-control mb-2" data-placeholder="Stream name" data-url="<%= url('/stream_names_filtering') %>" autocomplete="off">
12
12
  <option></option>
13
13
  <% if stream[:stream_name] %>
14
14
  <option value="<%= h stream[:stream_name] %>" selected><%= h stream[:stream_name] %></option>
15
15
  <% end %>
16
16
  </select>
17
17
  </div>
18
- <div class="col-4">
19
- <select name="filter[streams][][stream_id]" class="form-control mb-2" data-placeholder="Stream ID" data-url="<%= url('/stream_ids_filtering') %>">
18
+ <div class="col-3">
19
+ <select name="filter[streams][][stream_id]" class="form-control mb-2" data-placeholder="Stream ID" data-url="<%= url('/stream_ids_filtering') %>" autocomplete="off">
20
20
  <option></option>
21
21
  <% if stream[:stream_id] %>
22
22
  <option value="<%= h stream[:stream_id] %>" selected><%= h stream[:stream_id] %></option>
23
23
  <% end %>
24
24
  </select>
25
25
  </div>
26
- <div class="col-1">
26
+ <div class="col-3">
27
27
  <a class="btn btn-default remove-filter" href="javascript: void(0);">
28
28
  <i class="fa fa-minus-circle"></i>
29
29
  </a>
30
+ <% if stream[:context] && stream[:stream_name] && stream[:stream_id] %>
31
+ <a class="btn btn-danger btn-small delete-stream" data-confirm="You are about to delete all events in <%= h stream.inspect %> stream. This action is irreversible Continue?" data-confirm-title="Delete Stream" data-method="post" href="<%= delete_stream_url(stream) %>">
32
+ Delete stream
33
+ </a>
34
+ <% end %>
30
35
  </div>
31
36
  </div>
@@ -1,6 +1,6 @@
1
1
  <div class="form-row align-items-center">
2
2
  <div class="col-3">
3
- <select name="filter[system_stream]" class="form-control mb-2" data-placeholder="Select system stream">
3
+ <select name="filter[system_stream]" class="form-control mb-2" data-placeholder="Select system stream" autocomplete="off">
4
4
  <option></option>
5
5
  <% PgEventstore::Stream::KNOWN_SYSTEM_STREAMS.each do |stream_name| %>
6
6
  <option value="<%= stream_name %>" <% if stream == stream_name %> selected <% end %>><%= stream_name %></option>
@@ -95,6 +95,12 @@
95
95
  </li>
96
96
  </ul>
97
97
  </nav>
98
+ <div class="alert alert-success alert-dismissible fade show text-center d-none" id="flash-message" role="alert">
99
+ <h5 class="text-dark message"></h5>
100
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">
101
+ <span aria-hidden="true">&times;</span>
102
+ </button>
103
+ </div>
98
104
  </div>
99
105
  </div>
100
106
  <!-- /top navigation -->
@@ -133,12 +139,18 @@
133
139
  </div>
134
140
  </div>
135
141
 
142
+ <script type="text/javascript">
143
+ window.flashMessageCookie = "<%= PgEventstore::Web::Application::COOKIES_FLASH_MESSAGE_KEY %>";
144
+ </script>
145
+
136
146
  <!-- jQuery -->
137
147
  <script src="<%= asset_url("javascripts/vendor/jquery.min.js") %>"></script>
138
148
  <!-- Bootstrap -->
139
149
  <script src="<%= asset_url("javascripts/vendor/bootstrap.bundle.min.js") %>"></script>
140
150
  <script src="<%= asset_url("javascripts/vendor/select2.full.min.js") %>"></script>
141
151
 
152
+ <script src="<%= asset_url("javascripts/vendor/js.cookie.min.js") %>"></script>
153
+
142
154
  <!-- Custom Theme Scripts -->
143
155
  <script src="<%= asset_url("javascripts/gentelella.js") %>"></script>
144
156
  <script src="<%= asset_url("javascripts/pg_eventstore.js") %>"></script>
data/lib/pg_eventstore.rb CHANGED
@@ -11,7 +11,10 @@ require_relative 'pg_eventstore/event_class_resolver'
11
11
  require_relative 'pg_eventstore/config'
12
12
  require_relative 'pg_eventstore/event'
13
13
  require_relative 'pg_eventstore/stream'
14
+ require_relative 'pg_eventstore/commands'
15
+ require_relative 'pg_eventstore/queries'
14
16
  require_relative 'pg_eventstore/client'
17
+ require_relative 'pg_eventstore/maintenance'
15
18
  require_relative 'pg_eventstore/connection'
16
19
  require_relative 'pg_eventstore/errors'
17
20
  require_relative 'pg_eventstore/middleware'
@@ -96,6 +99,12 @@ module PgEventstore
96
99
  Client.new(config(name))
97
100
  end
98
101
 
102
+ # @param name [Symbol]
103
+ # @return [PgEventstore::Maintenance]
104
+ def maintenance(name = DEFAULT_CONFIG)
105
+ Maintenance.new(config(name))
106
+ end
107
+
99
108
  # @return [Logger, nil]
100
109
  def logger
101
110
  @logger
@@ -0,0 +1,13 @@
1
+ module PgEventstore
2
+ module Commands
3
+ class DeleteEvent
4
+ MAX_RECORDS_TO_LOCK: Integer
5
+
6
+ def call: (PgEventstore::Event event, force: bool)-> bool
7
+
8
+ private
9
+
10
+ def check_records_number_to_lock: (PgEventstore::Event event)-> void
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module PgEventstore
2
+ module Commands
3
+ class DeleteStream < PgEventstore::AbstractCommand
4
+ def call: (PgEventstore::Stream stream) -> bool
5
+ end
6
+ end
7
+ end
@@ -113,4 +113,14 @@ module PgEventstore
113
113
 
114
114
  attr_accessor event_types: ::Array[String]
115
115
  end
116
+
117
+ class TooManyRecordsToLockError < PgEventstore::Error
118
+ def initialize: (PgEventstore::Stream stream, Integer number_of_records) -> void
119
+
120
+ attr_accessor stream: PgEventstore::Stream
121
+
122
+ attr_accessor number_of_records: Integer
123
+
124
+ def user_friendly_message: -> String
125
+ end
116
126
  end
@@ -0,0 +1,19 @@
1
+ module PgEventstore
2
+ class Maintenance
3
+ @config: PgEventstore::Config
4
+
5
+ attr_reader config: PgEventstore::Config
6
+
7
+ def initialize: (PgEventstore::Config config) -> void
8
+
9
+ def connection: () -> PgEventstore::Connection
10
+
11
+ def delete_event: (PgEventstore::Event event, ?force: bool)-> bool
12
+
13
+ def delete_stream: (PgEventstore::Stream stream)-> bool
14
+
15
+ def maintenance_queries: () -> PgEventstore::MaintenanceQueries
16
+
17
+ def transaction_queries: () -> PgEventstore::TransactionQueries
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module PgEventstore
2
+ class MaintenanceQueries
3
+ attr_reader connection: PgEventstore::Connection
4
+
5
+ def initialize: (PgEventstore::Connection connection)-> untyped
6
+
7
+ def adjust_stream_revisions: (PgEventstore::Stream stream, Integer after_revision)-> void
8
+
9
+ def delete_event: (PgEventstore::Event event)-> Integer
10
+
11
+ def delete_stream: (PgEventstore::Stream stream)-> Integer
12
+
13
+ def events_to_lock_count: (PgEventstore::Stream stream, Integer after_revision)-> Integer
14
+
15
+ def reload_event: (PgEventstore::Event event)-> PgEventstore::Event?
16
+
17
+ private
18
+
19
+ def basic_deserializer: -> PgEventstore::EventDeserializer
20
+ end
21
+ end
@@ -2,6 +2,14 @@ module PgEventstore
2
2
  class Queries
3
3
  include PgEventstore::Extensions::OptionsExtension
4
4
 
5
+ attr_accessor events: PgEventstore::EventQueries?
6
+
7
+ attr_accessor partitions: PgEventstore::PartitionQueries?
8
+
9
+ attr_accessor transactions: PgEventstore::TransactionQueries?
10
+
11
+ attr_accessor maintenance: PgEventstore::MaintenanceQueries?
12
+
5
13
  def initialize: (**untyped options) -> void
6
14
 
7
15
  def options_hash: () -> ::Hash[untyped, untyped]
@@ -13,11 +21,5 @@ module PgEventstore
13
21
  def readonly_error: (Symbol opt_name) -> void
14
22
 
15
23
  def init_default_values: (::Hash[untyped, untyped] options) -> void
16
-
17
- attr_accessor events: PgEventstore::EventQueries?
18
-
19
- attr_accessor partitions: PgEventstore::PartitionQueries?
20
-
21
- attr_accessor transactions: PgEventstore::TransactionQueries?
22
24
  end
23
25
  end
@@ -2,6 +2,7 @@ module PgEventstore
2
2
  module Web
3
3
  class Application
4
4
  COOKIES_CONFIG_KEY: String
5
+ COOKIES_FLASH_MESSAGE_KEY: String
5
6
  DEFAULT_ADMIN_UI_CONFIG: Symbol
6
7
 
7
8
  def asset_url: (String path) -> String
@@ -14,6 +15,8 @@ module PgEventstore
14
15
 
15
16
  def events_filter: -> Array[String]?
16
17
 
18
+ def flash_message=: (({ message: String, kind: String }) val)-> String
19
+
17
20
  def h: (String text) -> String
18
21
 
19
22
  def paginated_json_response: (PgEventstore::Web::Paginator::BaseCollection collection) -> void
@@ -2,6 +2,10 @@ module PgEventstore
2
2
  module Web
3
3
  module Subscriptions
4
4
  module Helpers
5
+ def delete_event_url: (Integer global_position)-> String
6
+
7
+ def delete_stream_url: (({ context: String, stream_name: String, stream_id: String }) stream_attrs)-> String
8
+
5
9
  # _@param_ `set_name`
6
10
  def subscriptions_url: (?set_name: String?) -> String
7
11
 
@@ -14,6 +14,8 @@ module PgEventstore
14
14
  # _@param_ `name`
15
15
  def self.connection: (?Symbol name) -> PgEventstore::Connection
16
16
 
17
+ def self.maintenance: (?Symbol name) -> PgEventstore::Maintenance
18
+
17
19
  def self.subscriptions_manager: (
18
20
  ?Symbol config_name,
19
21
  subscription_set: 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.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Dzyzenko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-14 00:00:00.000000000 Z
11
+ date: 2025-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -85,6 +85,7 @@ files:
85
85
  - docs/events_and_streams.md
86
86
  - docs/how_it_works.md
87
87
  - docs/linking_events.md
88
+ - docs/maintenance.md
88
89
  - docs/multiple_commands.md
89
90
  - docs/reading_events.md
90
91
  - docs/subscriptions.md
@@ -116,6 +117,8 @@ files:
116
117
  - lib/pg_eventstore/client.rb
117
118
  - lib/pg_eventstore/commands.rb
118
119
  - lib/pg_eventstore/commands/append.rb
120
+ - lib/pg_eventstore/commands/delete_event.rb
121
+ - lib/pg_eventstore/commands/delete_stream.rb
119
122
  - lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb
120
123
  - lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb
121
124
  - lib/pg_eventstore/commands/link_to.rb
@@ -134,11 +137,13 @@ files:
134
137
  - lib/pg_eventstore/extensions/callbacks_extension.rb
135
138
  - lib/pg_eventstore/extensions/options_extension.rb
136
139
  - lib/pg_eventstore/extensions/using_connection_extension.rb
140
+ - lib/pg_eventstore/maintenance.rb
137
141
  - lib/pg_eventstore/middleware.rb
138
142
  - lib/pg_eventstore/pg_connection.rb
139
143
  - lib/pg_eventstore/queries.rb
140
144
  - lib/pg_eventstore/queries/event_queries.rb
141
145
  - lib/pg_eventstore/queries/links_resolver.rb
146
+ - lib/pg_eventstore/queries/maintenance_queries.rb
142
147
  - lib/pg_eventstore/queries/partition_queries.rb
143
148
  - lib/pg_eventstore/queries/transaction_queries.rb
144
149
  - lib/pg_eventstore/query_builders/events_filtering.rb
@@ -208,6 +213,7 @@ files:
208
213
  - lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js
209
214
  - lib/pg_eventstore/web/public/javascripts/vendor/jquery.autocomplete.min.js
210
215
  - lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js
216
+ - lib/pg_eventstore/web/public/javascripts/vendor/js.cookie.min.js
211
217
  - lib/pg_eventstore/web/public/javascripts/vendor/select2.full.min.js
212
218
  - lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css
213
219
  - lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css
@@ -266,6 +272,8 @@ files:
266
272
  - sig/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rbs
267
273
  - sig/pg_eventstore/client.rbs
268
274
  - sig/pg_eventstore/commands/append.rbs
275
+ - sig/pg_eventstore/commands/delete_event.rbs
276
+ - sig/pg_eventstore/commands/delete_stream.rbs
269
277
  - sig/pg_eventstore/commands/event_modifiers/prepare_link_event.rbs
270
278
  - sig/pg_eventstore/commands/event_modifiers/prepare_regular_event.rbs
271
279
  - sig/pg_eventstore/commands/link_to.rbs
@@ -284,11 +292,13 @@ files:
284
292
  - sig/pg_eventstore/extensions/callbacks_extension.rbs
285
293
  - sig/pg_eventstore/extensions/options_extension.rbs
286
294
  - sig/pg_eventstore/extensions/using_connection_extension.rbs
295
+ - sig/pg_eventstore/maintenance.rbs
287
296
  - sig/pg_eventstore/middleware.rbs
288
297
  - sig/pg_eventstore/pg_connection.rbs
289
298
  - sig/pg_eventstore/queries.rbs
290
299
  - sig/pg_eventstore/queries/event_queries.rbs
291
300
  - sig/pg_eventstore/queries/links_resolver.rbs
301
+ - sig/pg_eventstore/queries/maintenance_queries.rbs
292
302
  - sig/pg_eventstore/queries/partition_queries.rbs
293
303
  - sig/pg_eventstore/queries/transaction_queries.rbs
294
304
  - sig/pg_eventstore/query_builders/events_filtering_query.rbs