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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +1 -0
- data/db/migrations/8_improve_0_revision_partial_index.sql +2 -0
- data/docs/admin_ui.md +7 -1
- data/docs/configuration.md +3 -0
- data/docs/events_and_streams.md +18 -2
- data/docs/maintenance.md +51 -0
- data/docs/multiple_commands.md +4 -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/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +67 -6
- data/lib/pg_eventstore/web/paginator/events_collection.rb +2 -3
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +66 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/js.cookie.min.js +2 -0
- data/lib/pg_eventstore/web/subscriptions/helpers.rb +12 -0
- 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 +8 -2
- 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.rb +18 -6
- 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/web/application.rbs +8 -0
- data/sig/pg_eventstore/web/subscriptions/helpers.rbs +4 -0
- data/sig/pg_eventstore.rbs +4 -0
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c0936a5580f6340a43f65d5b1b3362855b30e78fb25bdd498bf21c759458fcb
|
4
|
+
data.tar.gz: d18a05c7c6917e66c83de05013afcf6b9f25aaf7027e32fb1345553f5398a120
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
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
|
|
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/events_and_streams.md
CHANGED
@@ -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
|
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
|
-
|
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
|
|
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/docs/multiple_commands.md
CHANGED
@@ -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.
|
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
|
@@ -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
|
-
|
38
|
-
|
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(
|
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
|
-
|
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
|
-
|
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">×</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
|
20
|
-
class="
|
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-
|
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>
|
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:
|
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 =
|
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 =
|
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 =
|
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 =
|
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 = {
|
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
|
@@ -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
|
|
data/sig/pg_eventstore.rbs
CHANGED
@@ -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.
|
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-
|
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
|