pg_eventstore 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -0
- data/docs/configuration.md +3 -0
- data/docs/maintenance.md +51 -0
- data/lib/pg_eventstore/client.rb +0 -2
- data/lib/pg_eventstore/commands/delete_event.rb +38 -0
- data/lib/pg_eventstore/commands/delete_stream.rb +18 -0
- data/lib/pg_eventstore/commands.rb +2 -0
- data/lib/pg_eventstore/errors.rb +19 -1
- data/lib/pg_eventstore/maintenance.rb +48 -0
- data/lib/pg_eventstore/queries/maintenance_queries.rb +81 -0
- data/lib/pg_eventstore/queries.rb +5 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +4 -1
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +70 -0
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +99 -4
- data/lib/pg_eventstore/web/public/javascripts/vendor/js.cookie.min.js +2 -0
- data/lib/pg_eventstore/web/subscriptions/helpers.rb +42 -16
- data/lib/pg_eventstore/web/views/home/dashboard.erb +32 -0
- data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +1 -1
- data/lib/pg_eventstore/web/views/home/partials/events.erb +15 -3
- data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +11 -6
- data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +1 -1
- data/lib/pg_eventstore/web/views/layouts/application.erb +12 -0
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +7 -5
- data/lib/pg_eventstore.rb +9 -0
- data/sig/pg_eventstore/commands/delete_event.rbs +13 -0
- data/sig/pg_eventstore/commands/delete_stream.rbs +7 -0
- data/sig/pg_eventstore/errors.rbs +10 -0
- data/sig/pg_eventstore/maintenance.rbs +19 -0
- data/sig/pg_eventstore/queries/maintenance_queries.rbs +21 -0
- data/sig/pg_eventstore/queries.rbs +8 -6
- data/sig/pg_eventstore/subscriptions/subscription.rbs +2 -0
- data/sig/pg_eventstore/web/application.rbs +3 -0
- data/sig/pg_eventstore/web/subscriptions/helpers.rbs +6 -0
- data/sig/pg_eventstore.rbs +2 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3ff0f3d0cf7c80ebe381ca1da32b921766c10055832f05b94d15e2f25b73d55
|
4
|
+
data.tar.gz: a00e2ef1bc9435b2e5b7732e0dd5ee85a3a3ed6e457c5d106f654f035ff7c3e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23c6bd66a79199cf49411e73635d4829c5bef802a97c3e37f3d46513a1c7b8163163fa665b99d47bd870829ba2059650c4d8b9798c95991c37834e478b352aa4
|
7
|
+
data.tar.gz: af188405be2d0a13c8694840dc6dcb5eef435ada27979a2f570be1ccacf33c465930d4bb9237ad6d7c78c7bde2edc5d0cfa61064dd7dc8c2c0c6a11856f39980
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.10.0]
|
4
|
+
- 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
|
+
- Admin IU: fixed several potential XSS vulnerabilities
|
6
|
+
- Admin IU: Add "Copy to clipboard" button near stream id that copies ruby stream definition
|
7
|
+
- Admin UI: allow deletion of streams with empty attribute values
|
8
|
+
|
9
|
+
## [1.9.0]
|
10
|
+
|
11
|
+
- Implement an ability to delete a stream
|
12
|
+
- Implement an ability to delete an event
|
13
|
+
- Add "Delete event" and "Delete stream" buttons into admin UI
|
14
|
+
|
3
15
|
## [1.8.0]
|
4
16
|
- Introduce default config for admin web UI. Now if you define `:admin_web_ui` config - it will be preferred over default config
|
5
17
|
- 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)
|
data/docs/configuration.md
CHANGED
@@ -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
|
data/docs/maintenance.md
ADDED
@@ -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.**
|
data/lib/pg_eventstore/client.rb
CHANGED
@@ -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'
|
data/lib/pg_eventstore/errors.rb
CHANGED
@@ -41,7 +41,7 @@ module PgEventstore
|
|
41
41
|
# @param stream [PgEventstore::Stream]
|
42
42
|
def initialize(stream)
|
43
43
|
@stream = stream
|
44
|
-
super("
|
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
|
@@ -6,6 +6,9 @@ module PgEventstore
|
|
6
6
|
include Extensions::UsingConnectionExtension
|
7
7
|
include Extensions::OptionsExtension
|
8
8
|
|
9
|
+
# @return [Time]
|
10
|
+
DEFAULT_TIMESTAMP = Time.at(0).utc.freeze
|
11
|
+
|
9
12
|
# @!attribute id
|
10
13
|
# @return [Integer, nil]
|
11
14
|
attribute(:id)
|
@@ -164,7 +167,7 @@ module PgEventstore
|
|
164
167
|
last_restarted_at: nil,
|
165
168
|
max_restarts_number: max_restarts_number,
|
166
169
|
chunk_query_interval: chunk_query_interval,
|
167
|
-
last_chunk_fed_at:
|
170
|
+
last_chunk_fed_at: DEFAULT_TIMESTAMP,
|
168
171
|
last_chunk_greatest_position: nil,
|
169
172
|
last_error: nil,
|
170
173
|
last_error_occurred_at: nil,
|
@@ -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,65 @@ 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' do
|
286
|
+
attrs = {
|
287
|
+
context: params[:context]&.to_s,
|
288
|
+
stream_name: params[:stream_name]&.to_s,
|
289
|
+
stream_id: params[:stream_id]&.to_s,
|
290
|
+
}
|
291
|
+
|
292
|
+
err_message = ->(attrs) {
|
293
|
+
self.flash_message = {
|
294
|
+
message: "Could not delete #{attrs}. It is not valid stream for deletion.",
|
295
|
+
kind: 'error'
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
if attrs.values.none?(&:nil?)
|
300
|
+
stream = PgEventstore::Stream.new(**attrs)
|
301
|
+
if stream.system?
|
302
|
+
err_message.call(stream.to_hash)
|
303
|
+
else
|
304
|
+
PgEventstore.maintenance(current_config).delete_stream(stream)
|
305
|
+
self.flash_message = {
|
306
|
+
message: "Stream #{stream.to_hash} has been successfully deleted.",
|
307
|
+
kind: 'success'
|
308
|
+
}
|
309
|
+
end
|
310
|
+
else
|
311
|
+
err_message.call(attrs)
|
312
|
+
end
|
313
|
+
|
314
|
+
redirect(redirect_back_url(fallback_url: '/'))
|
315
|
+
end
|
246
316
|
end
|
247
317
|
end
|
248
318
|
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){
|
@@ -188,14 +196,14 @@ $(function(){
|
|
188
196
|
let $confirmationModal = $('#confirmation-modal');
|
189
197
|
|
190
198
|
$confirmationModal.on('hide.bs.modal', function(){
|
191
|
-
$(this).find('.modal-title').
|
192
|
-
$(this).find('.modal-body').
|
199
|
+
$(this).find('.modal-title').text('');
|
200
|
+
$(this).find('.modal-body').text('');
|
193
201
|
$(this).find('.confirm').off();
|
194
202
|
});
|
195
203
|
let showConfirmation = function(el, callback){
|
196
204
|
let $el = $(el);
|
197
|
-
$confirmationModal.find('.modal-body').
|
198
|
-
$confirmationModal.find('.modal-title').
|
205
|
+
$confirmationModal.find('.modal-body').text($el.data('confirm'));
|
206
|
+
$confirmationModal.find('.modal-title').text($el.data('confirm-title'));
|
199
207
|
$confirmationModal.modal('show');
|
200
208
|
$confirmationModal.one('click', '.confirm', callback);
|
201
209
|
}
|
@@ -257,3 +265,90 @@ $(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
|
+
});
|
326
|
+
|
327
|
+
// Copy to clipboard with a tooltip implementation
|
328
|
+
$(function () {
|
329
|
+
"use strict";
|
330
|
+
|
331
|
+
let $selector = $(".copy-to-clipboard");
|
332
|
+
|
333
|
+
$selector.tooltip({
|
334
|
+
container: "body",
|
335
|
+
title: function(){
|
336
|
+
let $this = $(this);
|
337
|
+
|
338
|
+
return $this.data("temp-title") || $this.data("title");
|
339
|
+
}
|
340
|
+
});
|
341
|
+
|
342
|
+
$selector.click(function () {
|
343
|
+
let $this = $(this);
|
344
|
+
|
345
|
+
navigator.clipboard.writeText($this.data("clipboard-content"));
|
346
|
+
|
347
|
+
$this.data("temp-title", "Copied!");
|
348
|
+
$this.data("bs.tooltip").hide();
|
349
|
+
$this.one("shown.bs.tooltip", function(){
|
350
|
+
$this.data("temp-title", null);
|
351
|
+
});
|
352
|
+
$this.data("bs.tooltip").show();
|
353
|
+
});
|
354
|
+
});
|
@@ -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}));
|
@@ -79,34 +79,60 @@ module PgEventstore
|
|
79
79
|
# @param updated_at [Time]
|
80
80
|
# @return [String] html status
|
81
81
|
def colored_state(state, interval, updated_at)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
<<~HTML
|
89
|
-
<span class="text-warning text-nowrap">
|
90
|
-
#{state}
|
91
|
-
<i class="fa fa-question-circle" data-toggle="tooltip" title="#{title}"></i>
|
92
|
-
</span>
|
93
|
-
HTML
|
82
|
+
text_class =
|
83
|
+
case state
|
84
|
+
when RunnerState::STATES[:running]
|
85
|
+
alive?(interval, updated_at) ? 'text-success' : 'text-warning'
|
86
|
+
when RunnerState::STATES[:dead]
|
87
|
+
'text-danger'
|
94
88
|
else
|
95
|
-
|
89
|
+
'text-info'
|
96
90
|
end
|
97
|
-
|
98
|
-
|
91
|
+
|
92
|
+
if alive?(interval, updated_at)
|
93
|
+
<<~HTML
|
94
|
+
<span class="#{text_class}">#{state}</span>
|
95
|
+
HTML
|
99
96
|
else
|
100
|
-
|
97
|
+
title = <<~TEXT
|
98
|
+
Something is wrong. Last update was more than #{interval} seconds ago(#{updated_at}).
|
99
|
+
TEXT
|
100
|
+
<<~HTML
|
101
|
+
<span class="#{text_class} text-nowrap">
|
102
|
+
#{state}
|
103
|
+
<i class="fa fa-question-circle" data-toggle="tooltip" title="#{title}"></i>
|
104
|
+
</span>
|
105
|
+
HTML
|
101
106
|
end
|
102
107
|
end
|
103
108
|
|
109
|
+
# @param interval [Integer]
|
110
|
+
# @param last_updated_at [Time]
|
111
|
+
# @return [Boolean]
|
112
|
+
def alive?(interval, last_updated_at)
|
113
|
+
# -1 is added as a margin to prevent false-positive result
|
114
|
+
last_updated_at > Time.now.utc - interval - 1
|
115
|
+
end
|
116
|
+
|
104
117
|
# @param ids [Array<Integer>]
|
105
118
|
# @return [String]
|
106
119
|
def delete_all_subscriptions_url(ids)
|
107
120
|
encoded_params = Rack::Utils.build_nested_query(ids: ids)
|
108
121
|
url("/delete_all_subscriptions?#{encoded_params}")
|
109
122
|
end
|
123
|
+
|
124
|
+
# @param global_position [Integer]
|
125
|
+
# @return [String]
|
126
|
+
def delete_event_url(global_position)
|
127
|
+
url("/delete_event/#{global_position}")
|
128
|
+
end
|
129
|
+
|
130
|
+
# @param stream_attrs [Hash]
|
131
|
+
# @return [String]
|
132
|
+
def delete_stream_url(stream_attrs)
|
133
|
+
encoded_params = Rack::Utils.build_nested_query(stream_attrs)
|
134
|
+
url("/delete_stream?#{encoded_params}")
|
135
|
+
end
|
110
136
|
end
|
111
137
|
end
|
112
138
|
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">×</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>
|
@@ -4,7 +4,13 @@
|
|
4
4
|
<td><%= event.stream_revision %></td>
|
5
5
|
<td><%= h event.stream.context %></td>
|
6
6
|
<td><%= h event.stream.stream_name %></td>
|
7
|
-
<td
|
7
|
+
<td>
|
8
|
+
<a href="<%= stream_path(event) %>"><%= h event.stream.stream_id %></a>
|
9
|
+
<a role="button" href="#" data-title="Copy stream definition." class="copy-to-clipboard"
|
10
|
+
data-clipboard-content="<%= h "PgEventstore::Stream.new(context: #{event.stream.context.inspect}, stream_name: #{event.stream.stream_name.inspect}, stream_id: #{event.stream.stream_id.inspect})" %>">
|
11
|
+
<i class="fa fa-clipboard"></i>
|
12
|
+
</a>
|
13
|
+
</td>
|
8
14
|
<td>
|
9
15
|
<p class="float-left"><%= h event.type %></p>
|
10
16
|
<% if event.link %>
|
@@ -16,8 +22,14 @@
|
|
16
22
|
</td>
|
17
23
|
<td><%= event.created_at.strftime('%F %T') %></td>
|
18
24
|
<td><%= event.id %></td>
|
19
|
-
<td
|
20
|
-
class="
|
25
|
+
<td>
|
26
|
+
<a href="javascript: void(0);" class="d-inline-block text-nowrap toggle-event-data">
|
27
|
+
JSON <i class="fa fa-eye"></i>
|
28
|
+
</a>
|
29
|
+
<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">
|
30
|
+
Delete
|
31
|
+
</a>
|
32
|
+
</td>
|
21
33
|
</tr>
|
22
34
|
<tr class="event-payload d-none">
|
23
35
|
<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-
|
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-
|
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-
|
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">×</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>
|
@@ -85,14 +85,16 @@
|
|
85
85
|
Restore
|
86
86
|
</a>
|
87
87
|
<% end %>
|
88
|
-
<% if PgEventstore::RunnerState::STATES.values_at(:running, :dead).include?(subscriptions_set.state) %>
|
88
|
+
<% if PgEventstore::RunnerState::STATES.values_at(:running, :dead).include?(subscriptions_set.state) && alive?(PgEventstore::SubscriptionsSetLifecycle::HEARTBEAT_INTERVAL, subscriptions_set.updated_at) %>
|
89
89
|
<a class="btn btn-warning" data-confirm="You are about to stop SubscriptionsSet#<%= subscriptions_set.id %>. This will also delete it and will result in stopping all related Subscriptions. If you used pg_eventstore CLI to start subscriptions - the related process will also be stopped. Continue?" data-confirm-title="Stop SubscriptionsSet" data-method="post" href="<%= subscriptions_set_cmd_url(subscriptions_set.id, subscriptions_set_cmd('Stop')) %>" data-toggle="tooltip" title="This action will delete Subscriptions Set and release all related Subscriptions.">
|
90
90
|
Stop
|
91
91
|
</a>
|
92
92
|
<% end %>
|
93
|
-
|
94
|
-
Delete
|
95
|
-
|
93
|
+
<% unless alive?(PgEventstore::SubscriptionsSetLifecycle::HEARTBEAT_INTERVAL, subscriptions_set.updated_at) %>
|
94
|
+
<a class="btn btn-danger" data-confirm="You are about to delete SubscriptionsSet#<%= subscriptions_set.id %>. Continue?" data-confirm-title="Delete SubscriptionsSet" data-method="post" href="<%= url("/delete_subscriptions_set/#{subscriptions_set.id}") %>" data-toggle="tooltip" title="Use this action only on stuck Subscriptions Set - to clean it up.">
|
95
|
+
Delete
|
96
|
+
</a>
|
97
|
+
<% end %>
|
96
98
|
</td>
|
97
99
|
</tr>
|
98
100
|
<% if subscriptions_set.last_error %>
|
@@ -160,7 +162,7 @@
|
|
160
162
|
</td>
|
161
163
|
<td><%= subscription.current_position %></td>
|
162
164
|
<td><%= subscription.chunk_query_interval %>s</td>
|
163
|
-
<td><%= subscription.last_chunk_fed_at %></td>
|
165
|
+
<td><%= subscription.last_chunk_fed_at if subscription.last_chunk_fed_at > PgEventstore::Subscription::DEFAULT_TIMESTAMP %></td>
|
164
166
|
<td><%= colored_state(subscription.state, PgEventstore::SubscriptionsLifecycle::HEARTBEAT_INTERVAL, subscription.updated_at) %></td>
|
165
167
|
<td>
|
166
168
|
<% if subscription.average_event_processing_time %>
|
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
|
@@ -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,12 @@ module PgEventstore
|
|
2
2
|
module Web
|
3
3
|
module Subscriptions
|
4
4
|
module Helpers
|
5
|
+
def alive?: (Integer interval, Time last_updated_at)-> bool
|
6
|
+
|
7
|
+
def delete_event_url: (Integer global_position)-> String
|
8
|
+
|
9
|
+
def delete_stream_url: (({ context: String, stream_name: String, stream_id: String }) stream_attrs)-> String
|
10
|
+
|
5
11
|
# _@param_ `set_name`
|
6
12
|
def subscriptions_url: (?set_name: String?) -> String
|
7
13
|
|
data/sig/pg_eventstore.rbs
CHANGED
@@ -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.
|
4
|
+
version: 1.10.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-
|
11
|
+
date: 2025-04-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
|