pg_eventstore 1.7.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +1 -0
  4. data/db/migrations/8_improve_0_revision_partial_index.sql +2 -0
  5. data/docs/admin_ui.md +7 -1
  6. data/docs/configuration.md +3 -0
  7. data/docs/events_and_streams.md +18 -2
  8. data/docs/maintenance.md +51 -0
  9. data/docs/multiple_commands.md +4 -0
  10. data/lib/pg_eventstore/client.rb +0 -2
  11. data/lib/pg_eventstore/commands/delete_event.rb +38 -0
  12. data/lib/pg_eventstore/commands/delete_stream.rb +18 -0
  13. data/lib/pg_eventstore/commands.rb +2 -0
  14. data/lib/pg_eventstore/errors.rb +19 -1
  15. data/lib/pg_eventstore/maintenance.rb +48 -0
  16. data/lib/pg_eventstore/queries/maintenance_queries.rb +81 -0
  17. data/lib/pg_eventstore/queries.rb +5 -0
  18. data/lib/pg_eventstore/version.rb +1 -1
  19. data/lib/pg_eventstore/web/application.rb +67 -6
  20. data/lib/pg_eventstore/web/paginator/events_collection.rb +2 -3
  21. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +66 -0
  22. data/lib/pg_eventstore/web/public/javascripts/vendor/js.cookie.min.js +2 -0
  23. data/lib/pg_eventstore/web/subscriptions/helpers.rb +12 -0
  24. data/lib/pg_eventstore/web/views/home/dashboard.erb +32 -0
  25. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +1 -1
  26. data/lib/pg_eventstore/web/views/home/partials/events.erb +8 -2
  27. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +11 -6
  28. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +1 -1
  29. data/lib/pg_eventstore/web/views/layouts/application.erb +12 -0
  30. data/lib/pg_eventstore.rb +18 -6
  31. data/sig/pg_eventstore/commands/delete_event.rbs +13 -0
  32. data/sig/pg_eventstore/commands/delete_stream.rbs +7 -0
  33. data/sig/pg_eventstore/errors.rbs +10 -0
  34. data/sig/pg_eventstore/maintenance.rbs +19 -0
  35. data/sig/pg_eventstore/queries/maintenance_queries.rbs +21 -0
  36. data/sig/pg_eventstore/queries.rbs +8 -6
  37. data/sig/pg_eventstore/web/application.rbs +8 -0
  38. data/sig/pg_eventstore/web/subscriptions/helpers.rbs +4 -0
  39. data/sig/pg_eventstore.rbs +4 -0
  40. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff514958069cbf4b819b2ad959eac80ca9f24445bca19690ed4cb3ff23989804
4
- data.tar.gz: bd91be800c3739a43edf4c17c996156642f00f0d977446201c84784a1bcc6e74
3
+ metadata.gz: 7c0936a5580f6340a43f65d5b1b3362855b30e78fb25bdd498bf21c759458fcb
4
+ data.tar.gz: d18a05c7c6917e66c83de05013afcf6b9f25aaf7027e32fb1345553f5398a120
5
5
  SHA512:
6
- metadata.gz: 1d4946c58c314f0bfb04c0c20ca1308e4c8afe915adacee8722fcd703404b26dbc429bdda5bc5a44f94654cb366813065226b42c930620bf30fcd4a345f592b1
7
- data.tar.gz: 89738f55a626cbcd4eab9d336b7f4b9ff2e3520fc30a7fc7d87dfac5e755cd6431392438d1522cec706188aa855942db4cfd6fa96bc83c5e0b67086d6e05cef8
6
+ metadata.gz: b11fcbc08f2853a69c288dcd08498a5e40277cdeaf32ccba71ade48eac03cf3fd0c157abe0058daedb27aca945406afa8f3078f149e045ff63d89a47ca12da36
7
+ data.tar.gz: c021e192cfd8f61364235fa7ccdd2d97a37c39fe304f9167fe7cda508acd7c65b84bc2d168687a1cec185cf07870183b70fc849508a434ea13935398687cbea4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
9
+ ## [1.8.0]
10
+ - Introduce default config for admin web UI. Now if you define `:admin_web_ui` config - it will be preferred over default config
11
+ - Fix pagination of events in admin UI
12
+ - Improve partial index for `$streams` system stream
13
+
3
14
  ## [1.7.0]
4
15
  - Implement reading from `"$streams"` system stream
5
16
  - Disable Host authorization introduced in sinatra v4.1
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)
@@ -0,0 +1,2 @@
1
+ DROP INDEX IF EXISTS idx_events_0_stream_revision_global_position;
2
+ CREATE INDEX idx_events_0_stream_revision_global_position ON events USING btree (global_position) WHERE stream_revision = 0;
data/docs/admin_ui.md CHANGED
@@ -10,7 +10,13 @@
10
10
 
11
11
  ## Authorization
12
12
 
13
- Admin UI is implemented as a rack application. It doesn't have any built-in authentication/authorization mechanism - it is your responsibility to take care of it.
13
+ Admin UI is implemented as a rack application. It doesn't have any built-in authentication/authorization mechanism - it is your responsibility to take care of it. Admin UI tries to look for `:admin_web_ui` config with a fallback to `:default` config. Thus, you can setup Admin UI-specific config, e.g. without some middlewares or so. Example:
14
+
15
+ ```ruby
16
+ PgEventstore.configure(name: :admin_web_ui) do |config|
17
+ config.middlewares = { my_admin_ui_middleware: AdminUIMiddleware.new }
18
+ end
19
+ ```
14
20
 
15
21
  ### Rails integration
16
22
 
@@ -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
@@ -16,7 +16,9 @@
16
16
  - `stream_revision` - Integer(optional, read only). A revision of an event inside its stream.
17
17
  - `data` - Hash(optional). Event's payload data. For example, if you have a `DescriptionChanged` event class, then you may want to have a description value in the event payload data. Example: `DescriptionChanged.new(data: { 'description' => 'Description of something', 'post_id' => SecureRandom.uuid })`
18
18
  - `metadata` - Hash(optional). Event metadata. Event meta information which is not part of an events data payload. Example: `{ published_by: publishing_user.id }`
19
- - `link_id` - String(UUIDv4, optional, read only). If an event is a link event (link events are pointers to other events), this attribute contains the `id` of the original event. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
19
+ - `link_id` - String(UUIDv4, optional, read only). If an event is a link event (link events are pointers to other events), this attribute contains an `id` of the original event. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
20
+ - `link_partition_id` - Integer(optional, read only). If an event is a link event - this attribute contains a partition `id` of original event. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
21
+ - `link` - PgEventstore::Event(optional, read only). When reading from a stream using `resolve_link_tos: true`, if an event is resolved from a link - this attribute contains a `PgEventstore::Event` object which corresponds to that link. Manually assigning this attribute has no effect. It is internally set when reading events from the database.
20
22
  - `created_at` - Time(optional, read only). Database's timestamp when an event was appended to a stream. You may want to put your own timestamp into a `metadata` attribute - it may be useful when migrating between different databases. Manually assigning this attribute has no effect. It is internally set when appending an event to the given stream or when reading events from the database.
21
23
 
22
24
  Example:
@@ -40,7 +42,21 @@ PgEventstore::Stream.new(context: 'Sales', stream_name: 'Customer', stream_id: '
40
42
  PgEventstore::Stream.new(context: 'Sales', stream_name: 'Customer', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
41
43
  ```
42
44
 
43
- There is a special stream, called the "all" stream. You can get this object by calling the`PgEventstore::Stream.all_stream` method. Read more about the "all" stream in the `Reading from the "all" stream` section of [Reading events](reading_events.md) chapter.
45
+ ### "all" stream
46
+
47
+ There is a special stream, called the "all" stream. You can get this object by calling the `PgEventstore::Stream.all_stream` method. Read more about the "all" stream in the `Reading from the "all" stream` section of [Reading events](reading_events.md) chapter.
48
+
49
+ ### System streams
50
+
51
+ System stream is a special stream, the representation of which is pre-defined by the gem. System stream object can be created in next way:
52
+
53
+ ```ruby
54
+ PgEventstore::Stream.system_stream(stream_name)
55
+ ```
56
+
57
+ Current list of system streams is:
58
+
59
+ - `"$streams"`. Reading from this stream will return 0 revision events. This allows effectively loop through a list of streams. Read more in [Reading events](reading_events.md#streams-stream-filtering) chapter.
44
60
 
45
61
  ## Important note
46
62
 
@@ -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.**
@@ -44,3 +44,7 @@ old_email =
44
44
  # Sending email outside multiple block to prevent potential re-triggering of it
45
45
  UserMailer.notify_email_changed(user.id, old_email: old_email, new_email: user.email).deliver_later
46
46
  ```
47
+
48
+ ## Side effect of internal implementation
49
+
50
+ Please note that when publishing an event with a type as part of a `multiple` block that does not yet exist in the database, the block will run twice as the first attempt to publish will always fail due to the way `append_to_stream` is implemented. Consider this when writing expectations in your tests for example.
@@ -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.7.0"
5
+ VERSION = "1.9.0"
6
6
  end
@@ -1,10 +1,18 @@
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
7
8
  class Application < Sinatra::Base
9
+ # @return [Symbol]
10
+ DEFAULT_ADMIN_UI_CONFIG = :admin_web_ui
11
+ # @return [String]
12
+ COOKIES_CONFIG_KEY = 'current_config'
13
+ # @return [String]
14
+ COOKIES_FLASH_MESSAGE_KEY = 'flash_message'
15
+
8
16
  set :static_cache_control, [:private, max_age: 86400]
9
17
  set :environment, -> { (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['APP_ENV'])&.to_sym || :development }
10
18
  set :logging, -> { environment == :development || environment == :test }
@@ -34,14 +42,23 @@ module PgEventstore
34
42
 
35
43
  # @return [Symbol]
36
44
  def current_config
37
- config = request.cookies['current_config']&.to_s&.to_sym
38
- PgEventstore.available_configs.include?(config) ? config : :default
45
+ resolve_config_by_name(request.cookies[COOKIES_CONFIG_KEY]&.to_s&.to_sym)
46
+ end
47
+
48
+ # @param config_name [Symbol, nil]
49
+ # @return [Symbol]
50
+ def resolve_config_by_name(config_name)
51
+ existing_config = [config_name, DEFAULT_ADMIN_UI_CONFIG].find do |name|
52
+ PgEventstore.available_configs.include?(name)
53
+ end
54
+
55
+ existing_config || PgEventstore::DEFAULT_CONFIG
39
56
  end
40
57
 
41
58
  # @param val [Object]
42
59
  # @return [void]
43
60
  def current_config=(val)
44
- response.set_cookie('current_config', { value: val.to_s, http_only: true, same_site: :lax })
61
+ response.set_cookie(COOKIES_CONFIG_KEY, { value: val.to_s, http_only: true, same_site: :lax })
45
62
  end
46
63
 
47
64
  # @return [PgEventstore::Connection]
@@ -83,6 +100,14 @@ module PgEventstore
83
100
  def resolve_link_tos?
84
101
  params.key?(:resolve_link_tos) ? params[:resolve_link_tos] == 'true' : true
85
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
86
111
  end
87
112
 
88
113
  get '/' do
@@ -137,9 +162,7 @@ module PgEventstore
137
162
  end
138
163
 
139
164
  post '/change_config' do
140
- config = params[:config]&.to_sym
141
- config = :default unless PgEventstore.available_configs.include?(config)
142
- self.current_config = config
165
+ self.current_config = resolve_config_by_name(params[:config]&.to_s&.to_sym)
143
166
  redirect(redirect_back_url(fallback_url: '/'))
144
167
  end
145
168
 
@@ -231,6 +254,44 @@ module PgEventstore
231
254
 
232
255
  redirect redirect_back_url(fallback_url: url('/subscriptions'))
233
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
234
295
  end
235
296
  end
236
297
  end
@@ -34,8 +34,7 @@ module PgEventstore
34
34
  def collection
35
35
  @_collection ||= PgEventstore.client(config_name).read(
36
36
  @stream,
37
- options: options.merge(from_position: starting_id, max_count: per_page, direction: order),
38
- middlewares: []
37
+ options: options.merge(from_position: starting_id, max_count: per_page, direction: order)
39
38
  )
40
39
  end
41
40
 
@@ -60,7 +59,7 @@ module PgEventstore
60
59
  ).to_sql_builder.unselect.select('global_position').offset(1)
61
60
  sql, params = sql_builder.to_exec_params
62
61
  sql = "SELECT * FROM (#{sql}) events ORDER BY global_position #{order} LIMIT 1"
63
- PgEventstore.connection.with do |conn|
62
+ connection.with do |conn|
64
63
  conn.exec_params(sql, params)
65
64
  end.to_a.dig(0, 'global_position')
66
65
  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,13 +11,19 @@ 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'
18
21
  require_relative 'pg_eventstore/subscriptions/subscriptions_manager'
19
22
 
20
23
  module PgEventstore
24
+ # @return [Symbol]
25
+ DEFAULT_CONFIG = :default
26
+
21
27
  class << self
22
28
  # @!attribute mutex
23
29
  # @return [Thread::Mutex]
@@ -27,7 +33,7 @@ module PgEventstore
27
33
  # Creates a Config if not exists and yields it to the given block.
28
34
  # @param name [Symbol] a name to assign to a config
29
35
  # @return [Object] a result of the given block
30
- def configure(name: :default)
36
+ def configure(name: DEFAULT_CONFIG)
31
37
  mutex.synchronize do
32
38
  @config[name] ||= Config.new(name: name)
33
39
  connection_config_was = @config[name].connection_options
@@ -48,7 +54,7 @@ module PgEventstore
48
54
 
49
55
  # @param name [Symbol]
50
56
  # @return [PgEventstore::Config]
51
- def config(name = :default)
57
+ def config(name = DEFAULT_CONFIG)
52
58
  return @config[name] if @config[name]
53
59
 
54
60
  error_message = <<~TEXT
@@ -64,7 +70,7 @@ module PgEventstore
64
70
  # thread-safe
65
71
  # @param name [Symbol]
66
72
  # @return [PgEventstore::Connection]
67
- def connection(name = :default)
73
+ def connection(name = DEFAULT_CONFIG)
68
74
  mutex.synchronize do
69
75
  @connection[name] ||= Connection.new(**config(name).connection_options)
70
76
  end
@@ -76,7 +82,7 @@ module PgEventstore
76
82
  # @param retries_interval [Integer, nil] a delay between retries of failed SubscriptionsSet
77
83
  # @param force_lock [Boolean] whether to force-lock subscriptions
78
84
  # @return [PgEventstore::SubscriptionsManager]
79
- def subscriptions_manager(config_name = :default, subscription_set:, max_retries: nil, retries_interval: nil,
85
+ def subscriptions_manager(config_name = DEFAULT_CONFIG, subscription_set:, max_retries: nil, retries_interval: nil,
80
86
  force_lock: false)
81
87
  SubscriptionsManager.new(
82
88
  config: config(config_name),
@@ -89,10 +95,16 @@ module PgEventstore
89
95
 
90
96
  # @param name [Symbol]
91
97
  # @return [PgEventstore::Client]
92
- def client(name = :default)
98
+ def client(name = DEFAULT_CONFIG)
93
99
  Client.new(config(name))
94
100
  end
95
101
 
102
+ # @param name [Symbol]
103
+ # @return [PgEventstore::Maintenance]
104
+ def maintenance(name = DEFAULT_CONFIG)
105
+ Maintenance.new(config(name))
106
+ end
107
+
96
108
  # @return [Logger, nil]
97
109
  def logger
98
110
  @logger
@@ -108,7 +120,7 @@ module PgEventstore
108
120
 
109
121
  # @return [void]
110
122
  def init_variables
111
- @config = { default: Config.new(name: :default) }
123
+ @config = { DEFAULT_CONFIG => Config.new(name: DEFAULT_CONFIG) }
112
124
  @connection = {}
113
125
  @mutex = Thread::Mutex.new
114
126
  end
@@ -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
@@ -1,6 +1,10 @@
1
1
  module PgEventstore
2
2
  module Web
3
3
  class Application
4
+ COOKIES_CONFIG_KEY: String
5
+ COOKIES_FLASH_MESSAGE_KEY: String
6
+ DEFAULT_ADMIN_UI_CONFIG: Symbol
7
+
4
8
  def asset_url: (String path) -> String
5
9
 
6
10
  def connection: -> PgEventstore::Connection
@@ -11,12 +15,16 @@ module PgEventstore
11
15
 
12
16
  def events_filter: -> Array[String]?
13
17
 
18
+ def flash_message=: (({ message: String, kind: String }) val)-> String
19
+
14
20
  def h: (String text) -> String
15
21
 
16
22
  def paginated_json_response: (PgEventstore::Web::Paginator::BaseCollection collection) -> void
17
23
 
18
24
  def redirect_back_url: (fallback_url: String) -> String
19
25
 
26
+ def resolve_config_by_name: (Symbol? config_name)-> Symbol
27
+
20
28
  def resolve_link_tos?: -> bool
21
29
 
22
30
  def streams_filter: -> Array[Hash[untyped, untyped]]?
@@ -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
 
@@ -1,4 +1,6 @@
1
1
  module PgEventstore
2
+ DEFAULT_CONFIG: Symbol
3
+
2
4
  # _@param_ `name` — a name to assign to a config
3
5
  #
4
6
  # _@return_ — a result of the given block
@@ -12,6 +14,8 @@ module PgEventstore
12
14
  # _@param_ `name`
13
15
  def self.connection: (?Symbol name) -> PgEventstore::Connection
14
16
 
17
+ def self.maintenance: (?Symbol name) -> PgEventstore::Maintenance
18
+
15
19
  def self.subscriptions_manager: (
16
20
  ?Symbol config_name,
17
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.7.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-01-10 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
@@ -78,12 +78,14 @@ files:
78
78
  - db/migrations/5_partitions.sql
79
79
  - db/migrations/6_add_commands_data.sql
80
80
  - db/migrations/7_support_reading_streams_system_stream.sql
81
+ - db/migrations/8_improve_0_revision_partial_index.sql
81
82
  - docs/admin_ui.md
82
83
  - docs/appending_events.md
83
84
  - docs/configuration.md
84
85
  - docs/events_and_streams.md
85
86
  - docs/how_it_works.md
86
87
  - docs/linking_events.md
88
+ - docs/maintenance.md
87
89
  - docs/multiple_commands.md
88
90
  - docs/reading_events.md
89
91
  - docs/subscriptions.md
@@ -115,6 +117,8 @@ files:
115
117
  - lib/pg_eventstore/client.rb
116
118
  - lib/pg_eventstore/commands.rb
117
119
  - lib/pg_eventstore/commands/append.rb
120
+ - lib/pg_eventstore/commands/delete_event.rb
121
+ - lib/pg_eventstore/commands/delete_stream.rb
118
122
  - lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb
119
123
  - lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb
120
124
  - lib/pg_eventstore/commands/link_to.rb
@@ -133,11 +137,13 @@ files:
133
137
  - lib/pg_eventstore/extensions/callbacks_extension.rb
134
138
  - lib/pg_eventstore/extensions/options_extension.rb
135
139
  - lib/pg_eventstore/extensions/using_connection_extension.rb
140
+ - lib/pg_eventstore/maintenance.rb
136
141
  - lib/pg_eventstore/middleware.rb
137
142
  - lib/pg_eventstore/pg_connection.rb
138
143
  - lib/pg_eventstore/queries.rb
139
144
  - lib/pg_eventstore/queries/event_queries.rb
140
145
  - lib/pg_eventstore/queries/links_resolver.rb
146
+ - lib/pg_eventstore/queries/maintenance_queries.rb
141
147
  - lib/pg_eventstore/queries/partition_queries.rb
142
148
  - lib/pg_eventstore/queries/transaction_queries.rb
143
149
  - lib/pg_eventstore/query_builders/events_filtering.rb
@@ -207,6 +213,7 @@ files:
207
213
  - lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js
208
214
  - lib/pg_eventstore/web/public/javascripts/vendor/jquery.autocomplete.min.js
209
215
  - lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js
216
+ - lib/pg_eventstore/web/public/javascripts/vendor/js.cookie.min.js
210
217
  - lib/pg_eventstore/web/public/javascripts/vendor/select2.full.min.js
211
218
  - lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css
212
219
  - lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css
@@ -265,6 +272,8 @@ files:
265
272
  - sig/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rbs
266
273
  - sig/pg_eventstore/client.rbs
267
274
  - sig/pg_eventstore/commands/append.rbs
275
+ - sig/pg_eventstore/commands/delete_event.rbs
276
+ - sig/pg_eventstore/commands/delete_stream.rbs
268
277
  - sig/pg_eventstore/commands/event_modifiers/prepare_link_event.rbs
269
278
  - sig/pg_eventstore/commands/event_modifiers/prepare_regular_event.rbs
270
279
  - sig/pg_eventstore/commands/link_to.rbs
@@ -283,11 +292,13 @@ files:
283
292
  - sig/pg_eventstore/extensions/callbacks_extension.rbs
284
293
  - sig/pg_eventstore/extensions/options_extension.rbs
285
294
  - sig/pg_eventstore/extensions/using_connection_extension.rbs
295
+ - sig/pg_eventstore/maintenance.rbs
286
296
  - sig/pg_eventstore/middleware.rbs
287
297
  - sig/pg_eventstore/pg_connection.rbs
288
298
  - sig/pg_eventstore/queries.rbs
289
299
  - sig/pg_eventstore/queries/event_queries.rbs
290
300
  - sig/pg_eventstore/queries/links_resolver.rbs
301
+ - sig/pg_eventstore/queries/maintenance_queries.rbs
291
302
  - sig/pg_eventstore/queries/partition_queries.rbs
292
303
  - sig/pg_eventstore/queries/transaction_queries.rbs
293
304
  - sig/pg_eventstore/query_builders/events_filtering_query.rbs