pg_eventstore 0.10.1 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +26 -0
- data/db/migrations/1_create_events.sql +12 -11
- data/db/migrations/2_create_subscriptions.sql +6 -2
- data/db/migrations/3_create_subscription_commands.sql +9 -5
- data/db/migrations/4_create_subscriptions_set_commands.sql +1 -1
- data/db/migrations/5_partitions.sql +1 -0
- data/docs/configuration.md +1 -1
- data/docs/how_it_works.md +14 -1
- data/lib/pg_eventstore/commands/append.rb +1 -1
- data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +30 -8
- data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +8 -10
- data/lib/pg_eventstore/commands/link_to.rb +14 -7
- data/lib/pg_eventstore/errors.rb +10 -12
- data/lib/pg_eventstore/event.rb +9 -1
- data/lib/pg_eventstore/event_deserializer.rb +1 -0
- data/lib/pg_eventstore/queries/event_queries.rb +33 -6
- data/lib/pg_eventstore/queries/links_resolver.rb +53 -0
- data/lib/pg_eventstore/queries/partition_queries.rb +8 -0
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +27 -7
- data/lib/pg_eventstore/queries/subscription_queries.rb +70 -35
- data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +13 -1
- data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +18 -4
- data/lib/pg_eventstore/queries.rb +1 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +4 -17
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +10 -2
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +9 -7
- data/lib/pg_eventstore/subscriptions/commands_handler.rb +3 -2
- data/lib/pg_eventstore/subscriptions/events_processor.rb +10 -2
- data/lib/pg_eventstore/subscriptions/subscription.rb +29 -12
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +20 -16
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +1 -1
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +3 -4
- data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +22 -1
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +180 -0
- data/lib/pg_eventstore/web/paginator/base_collection.rb +56 -0
- data/lib/pg_eventstore/web/paginator/event_types_collection.rb +50 -0
- data/lib/pg_eventstore/web/paginator/events_collection.rb +105 -0
- data/lib/pg_eventstore/web/paginator/helpers.rb +119 -0
- data/lib/pg_eventstore/web/paginator/stream_contexts_collection.rb +48 -0
- data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +50 -0
- data/lib/pg_eventstore/web/paginator/stream_names_collection.rb +51 -0
- data/lib/pg_eventstore/web/public/fonts/vendor/FontAwesome.otf +0 -0
- data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.eot +0 -0
- data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.svg +685 -0
- data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.ttf +0 -0
- data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.woff +0 -0
- data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.woff2 +0 -0
- data/lib/pg_eventstore/web/public/images/favicon.ico +0 -0
- data/lib/pg_eventstore/web/public/javascripts/gentelella.js +334 -0
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +162 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js +7 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js.map +1 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.autocomplete.min.js +8 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js +4 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js.map +1 -0
- data/lib/pg_eventstore/web/public/javascripts/vendor/select2.full.min.js +2 -0
- data/lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css +5 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css +7 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css.map +1 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/font-awesome.min.css +4 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/font-awesome.min.css.map +7 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/gentelella.min.css +13 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/select2-bootstrap4.min.css +3 -0
- data/lib/pg_eventstore/web/public/stylesheets/vendor/select2.min.css +2 -0
- data/lib/pg_eventstore/web/subscriptions/helpers.rb +76 -0
- data/lib/pg_eventstore/web/subscriptions/set_collection.rb +34 -0
- data/lib/pg_eventstore/web/subscriptions/subscriptions.rb +33 -0
- data/lib/pg_eventstore/web/subscriptions/subscriptions_set.rb +33 -0
- data/lib/pg_eventstore/web/subscriptions/subscriptions_to_set_association.rb +32 -0
- data/lib/pg_eventstore/web/views/home/dashboard.erb +147 -0
- data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +15 -0
- data/lib/pg_eventstore/web/views/home/partials/events.erb +22 -0
- data/lib/pg_eventstore/web/views/home/partials/pagination_links.erb +3 -0
- data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +31 -0
- data/lib/pg_eventstore/web/views/layouts/application.erb +116 -0
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +220 -0
- data/lib/pg_eventstore/web.rb +22 -0
- data/lib/pg_eventstore.rb +5 -0
- data/pg_eventstore.gemspec +2 -1
- metadata +61 -2
@@ -22,7 +22,7 @@ module PgEventstore
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# @!attribute id
|
25
|
-
# @return [
|
25
|
+
# @return [Integer] It is used to lock the Subscription by updating Subscription#locked_by attribute
|
26
26
|
attribute(:id)
|
27
27
|
# @!attribute name
|
28
28
|
# @return [String] name of the set
|
@@ -87,6 +87,27 @@ module PgEventstore
|
|
87
87
|
self
|
88
88
|
end
|
89
89
|
|
90
|
+
# @return [Integer]
|
91
|
+
def hash
|
92
|
+
id.hash
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param another [Object]
|
96
|
+
# @return [Boolean]
|
97
|
+
def eql?(another)
|
98
|
+
return false unless another.is_a?(SubscriptionsSet)
|
99
|
+
|
100
|
+
hash == another.hash
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param another [PgEventstore::SubscriptionsSet]
|
104
|
+
# @return [Boolean]
|
105
|
+
def ==(another)
|
106
|
+
return false unless another.is_a?(SubscriptionsSet)
|
107
|
+
|
108
|
+
id == another.id
|
109
|
+
end
|
110
|
+
|
90
111
|
private
|
91
112
|
|
92
113
|
# @return [PgEventstore::SubscriptionsSetQueries]
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module PgEventstore
|
6
|
+
module Web
|
7
|
+
class Application < Sinatra::Base
|
8
|
+
set :static_cache_control, [:private, max_age: 86400]
|
9
|
+
set :environment, -> { (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['APP_ENV'])&.to_sym || :development }
|
10
|
+
set :logging, -> { environment == :development || environment == :test }
|
11
|
+
set :erb, layout: :'layouts/application'
|
12
|
+
set :sessions, true
|
13
|
+
set :session_secret, ENV.fetch('SECRET_KEY_BASE') { SecureRandom.hex(64) }
|
14
|
+
|
15
|
+
helpers(Paginator::Helpers, Subscriptions::Helpers) do
|
16
|
+
# @return [Array<Hash>, nil]
|
17
|
+
def streams_filter
|
18
|
+
params in { filter: { streams: Array => streams } }
|
19
|
+
streams&.select { _1 in { context: String, stream_name: String, stream_id: String } }&.map do
|
20
|
+
Hash[_1.reject { |_, value| value == '' }].transform_keys(&:to_sym)
|
21
|
+
end&.reject { _1.empty? }
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Array<String>, nil]
|
25
|
+
def events_filter
|
26
|
+
params in { filter: { events: Array => events } }
|
27
|
+
events&.select { _1.is_a?(String) && _1 != '' }
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Symbol]
|
31
|
+
def current_config
|
32
|
+
PgEventstore.available_configs.include?(session[:current_config]) ? session[:current_config] : :default
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [PgEventstore::Connection]
|
36
|
+
def connection
|
37
|
+
PgEventstore.connection(current_config)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param collection [PgEventstore::Paginator::BaseCollection]
|
41
|
+
# @return [void]
|
42
|
+
def paginated_json_response(collection)
|
43
|
+
halt 200, {
|
44
|
+
results: collection.collection,
|
45
|
+
pagination: { more: !collection.next_page_starting_id.nil?, starting_id: collection.next_page_starting_id }
|
46
|
+
}.to_json
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param fallback_url [String]
|
50
|
+
# @return [String]
|
51
|
+
def redirect_back_url(fallback_url:)
|
52
|
+
return fallback_url if request.referer.to_s.empty?
|
53
|
+
|
54
|
+
"#{request.referer}#{params[:hash]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
get '/' do
|
59
|
+
@collection = Paginator::EventsCollection.new(
|
60
|
+
current_config,
|
61
|
+
starting_id: params[:starting_id]&.to_i,
|
62
|
+
per_page: Paginator::EventsCollection::PER_PAGE[params[:per_page]],
|
63
|
+
order: Paginator::EventsCollection::SQL_DIRECTIONS[params[:order]],
|
64
|
+
options: { filter: { event_types: events_filter, streams: streams_filter } }
|
65
|
+
)
|
66
|
+
|
67
|
+
if request.xhr?
|
68
|
+
content_type 'application/json'
|
69
|
+
halt 200, {
|
70
|
+
events: erb(:'home/partials/events', { layout: false }, { events: @collection.collection }),
|
71
|
+
total_count: total_count(@collection.total_count),
|
72
|
+
pagination: erb(:'home/partials/pagination_links', { layout: false }, { collection: @collection })
|
73
|
+
}.to_json
|
74
|
+
else
|
75
|
+
erb :'home/dashboard'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
get '/subscriptions' do
|
80
|
+
@set_collection = Subscriptions::SetCollection.new(connection)
|
81
|
+
@current_set = params[:set_name] || @set_collection.names.first
|
82
|
+
@association = Subscriptions::SubscriptionsToSetAssociation.new(
|
83
|
+
subscriptions_set: Subscriptions::SubscriptionsSet.new(connection, @current_set).subscriptions_set,
|
84
|
+
subscriptions: Subscriptions::Subscriptions.new(connection, @current_set).subscriptions
|
85
|
+
)
|
86
|
+
erb :'subscriptions/index'
|
87
|
+
end
|
88
|
+
|
89
|
+
post '/change_config' do
|
90
|
+
config = params[:config]&.to_sym
|
91
|
+
config = :default unless PgEventstore.available_configs.include?(config)
|
92
|
+
session[:current_config] = config
|
93
|
+
redirect(url('/'))
|
94
|
+
end
|
95
|
+
|
96
|
+
get '/stream_contexts_filtering', provides: :json do
|
97
|
+
collection = Paginator::StreamContextsCollection.new(
|
98
|
+
current_config,
|
99
|
+
starting_id: params[:starting_id],
|
100
|
+
per_page: Paginator::StreamContextsCollection::PER_PAGE,
|
101
|
+
order: :asc,
|
102
|
+
options: { query: params[:term] }
|
103
|
+
)
|
104
|
+
paginated_json_response(collection)
|
105
|
+
end
|
106
|
+
|
107
|
+
get '/stream_names_filtering', provides: :json do
|
108
|
+
collection = Paginator::StreamNamesCollection.new(
|
109
|
+
current_config,
|
110
|
+
starting_id: params[:starting_id],
|
111
|
+
per_page: Paginator::StreamNamesCollection::PER_PAGE,
|
112
|
+
order: :asc,
|
113
|
+
options: { query: params[:term], context: params[:context] }
|
114
|
+
)
|
115
|
+
paginated_json_response(collection)
|
116
|
+
end
|
117
|
+
|
118
|
+
get '/stream_ids_filtering', provides: :json do
|
119
|
+
collection = Paginator::StreamIdsCollection.new(
|
120
|
+
current_config,
|
121
|
+
starting_id: params[:starting_id],
|
122
|
+
per_page: Paginator::StreamIdsCollection::PER_PAGE,
|
123
|
+
order: :asc,
|
124
|
+
options: { query: params[:term], context: params[:context], stream_name: params[:stream_name] }
|
125
|
+
)
|
126
|
+
paginated_json_response(collection)
|
127
|
+
end
|
128
|
+
|
129
|
+
get '/event_types_filtering', provides: :json do
|
130
|
+
collection = Paginator::EventTypesCollection.new(
|
131
|
+
current_config,
|
132
|
+
starting_id: params[:starting_id],
|
133
|
+
per_page: Paginator::EventTypesCollection::PER_PAGE,
|
134
|
+
order: :asc,
|
135
|
+
options: { query: params[:term] }
|
136
|
+
)
|
137
|
+
paginated_json_response(collection)
|
138
|
+
end
|
139
|
+
|
140
|
+
post '/subscription_cmd/:set_id/:id/:cmd' do
|
141
|
+
puts SubscriptionCommandQueries.new(connection).find_or_create_by(
|
142
|
+
subscriptions_set_id: params[:set_id],
|
143
|
+
subscription_id: params[:id],
|
144
|
+
command_name: CommandHandlers::SubscriptionRunnersCommands::AVAILABLE_COMMANDS.fetch(params[:cmd].to_sym)
|
145
|
+
)
|
146
|
+
|
147
|
+
redirect redirect_back_url(fallback_url: url('/subscriptions'))
|
148
|
+
end
|
149
|
+
|
150
|
+
post '/subscriptions_set_cmd/:id/:cmd' do
|
151
|
+
SubscriptionsSetCommandQueries.new(connection).find_or_create_by(
|
152
|
+
subscriptions_set_id: params[:id],
|
153
|
+
command_name: CommandHandlers::SubscriptionFeederCommands::AVAILABLE_COMMANDS.fetch(params[:cmd].to_sym)
|
154
|
+
)
|
155
|
+
|
156
|
+
redirect redirect_back_url(fallback_url: url('/subscriptions'))
|
157
|
+
end
|
158
|
+
|
159
|
+
post '/delete_subscriptions_set/:id' do
|
160
|
+
SubscriptionsSetQueries.new(connection).delete(params[:id])
|
161
|
+
|
162
|
+
redirect redirect_back_url(fallback_url: url('/subscriptions'))
|
163
|
+
end
|
164
|
+
|
165
|
+
post '/delete_subscription/:id' do
|
166
|
+
SubscriptionQueries.new(connection).delete(params[:id])
|
167
|
+
|
168
|
+
redirect redirect_back_url(fallback_url: url('/subscriptions'))
|
169
|
+
end
|
170
|
+
|
171
|
+
post '/delete_all_subscriptions' do
|
172
|
+
params[:ids].each do |id|
|
173
|
+
SubscriptionQueries.new(connection).delete(id)
|
174
|
+
end
|
175
|
+
|
176
|
+
redirect redirect_back_url(fallback_url: url('/subscriptions'))
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Web
|
5
|
+
module Paginator
|
6
|
+
class BaseCollection
|
7
|
+
attr_reader :config_name, :starting_id, :per_page, :order, :options
|
8
|
+
|
9
|
+
# @param config_name [Symbol]
|
10
|
+
# @param starting_id [String, Integer, nil]
|
11
|
+
# @param per_page [Integer]
|
12
|
+
# @param order [Symbol] :asc or :desc
|
13
|
+
# @param options [Hash] additional options to filter the collection
|
14
|
+
def initialize(config_name, starting_id:, per_page:, order:, options: {})
|
15
|
+
@config_name = config_name
|
16
|
+
@starting_id = starting_id
|
17
|
+
@per_page = per_page
|
18
|
+
@order = order
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Array]
|
23
|
+
def collection
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Integer]
|
28
|
+
def count
|
29
|
+
collection.size
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [String, Integer, nil]
|
33
|
+
def next_page_starting_id
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String, Integer, nil]
|
38
|
+
def prev_page_starting_id
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Integer]
|
43
|
+
def total_count
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @return [PgEventstore::Connection]
|
50
|
+
def connection
|
51
|
+
PgEventstore.connection(config_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Web
|
5
|
+
module Paginator
|
6
|
+
class EventTypesCollection < BaseCollection
|
7
|
+
PER_PAGE = 10
|
8
|
+
|
9
|
+
# @return [Array<Hash>]
|
10
|
+
def collection
|
11
|
+
@_collection ||=
|
12
|
+
begin
|
13
|
+
sql_builder =
|
14
|
+
SQLBuilder.new.select('event_type').from('partitions').
|
15
|
+
where('context is not null and stream_name is not null').
|
16
|
+
group('event_type').order("event_type #{order}").limit(per_page)
|
17
|
+
sql_builder.where("event_type #{direction_operator} ?", starting_id) if starting_id
|
18
|
+
sql_builder.where('event_type like ?', "#{options[:query]}%")
|
19
|
+
connection.with do |conn|
|
20
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
21
|
+
end.to_a
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String, nil]
|
26
|
+
def next_page_starting_id
|
27
|
+
return unless collection.size == per_page
|
28
|
+
|
29
|
+
starting_id = collection.first['event_type']
|
30
|
+
sql_builder =
|
31
|
+
SQLBuilder.new.select('event_type').from('partitions').
|
32
|
+
where('context is not null and stream_name is not null').
|
33
|
+
where("event_type #{direction_operator} ?", starting_id).where('event_type like ?', "#{options[:query]}%").
|
34
|
+
group('event_type').order("event_type #{order}").limit(1).offset(per_page)
|
35
|
+
|
36
|
+
connection.with do |conn|
|
37
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
38
|
+
end.to_a.dig(0, 'event_type')
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @return [String]
|
44
|
+
def direction_operator
|
45
|
+
order == :asc ? '>=' : '<='
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Web
|
5
|
+
module Paginator
|
6
|
+
class EventsCollection < BaseCollection
|
7
|
+
SQL_DIRECTIONS = {
|
8
|
+
'asc' => :asc,
|
9
|
+
'desc' => :desc
|
10
|
+
}.tap do |directions|
|
11
|
+
directions.default = :desc
|
12
|
+
end.freeze
|
13
|
+
PER_PAGE = %w[10 20 50 100 1000].to_h { [_1, _1.to_i] }.tap do |per_page|
|
14
|
+
per_page.default = 10
|
15
|
+
end.freeze
|
16
|
+
# Max number of events after which we don't perform the exact count and keep the estimate count instead because of
|
17
|
+
# the potential performance degradation.
|
18
|
+
MAX_NUMBER_TO_COUNT = 10_000
|
19
|
+
|
20
|
+
# @return [Array<PgEventstore::Event>]
|
21
|
+
def collection
|
22
|
+
@_collection ||= PgEventstore.client(config_name).read(
|
23
|
+
PgEventstore::Stream.all_stream,
|
24
|
+
options: options.merge(
|
25
|
+
from_position: starting_id, max_count: per_page, direction: order, resolve_link_tos: true
|
26
|
+
),
|
27
|
+
middlewares: []
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Integer, nil]
|
32
|
+
def next_page_starting_id
|
33
|
+
return unless collection.size == per_page
|
34
|
+
|
35
|
+
from_position = event_global_position(collection.first)
|
36
|
+
sql_builder = QueryBuilders::EventsFiltering.all_stream_filtering(
|
37
|
+
options.merge(from_position: from_position, max_count: 1, direction: order)
|
38
|
+
).to_sql_builder.unselect.select('global_position').offset(per_page)
|
39
|
+
global_position(sql_builder)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Integer, nil]
|
43
|
+
def prev_page_starting_id
|
44
|
+
from_position = event_global_position(collection.first) || starting_id
|
45
|
+
sql_builder = QueryBuilders::EventsFiltering.all_stream_filtering(
|
46
|
+
options.merge(from_position: from_position, max_count: per_page, direction: order == :asc ? :desc : :asc)
|
47
|
+
).to_sql_builder.unselect.select('global_position').offset(1)
|
48
|
+
sql, params = sql_builder.to_exec_params
|
49
|
+
sql = "SELECT * FROM (#{sql}) events ORDER BY global_position #{order} LIMIT 1"
|
50
|
+
PgEventstore.connection.with do |conn|
|
51
|
+
conn.exec_params(sql, params)
|
52
|
+
end.to_a.dig(0, 'global_position')
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Integer]
|
56
|
+
def total_count
|
57
|
+
@_total_count ||=
|
58
|
+
begin
|
59
|
+
sql_builder =
|
60
|
+
QueryBuilders::EventsFiltering.all_stream_filtering(options).
|
61
|
+
to_sql_builder.remove_limit.remove_group.remove_order
|
62
|
+
count = estimate_count(sql_builder)
|
63
|
+
return count if count > MAX_NUMBER_TO_COUNT
|
64
|
+
|
65
|
+
regular_count(sql_builder)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# @param event [PgEventstore::Eventg]
|
72
|
+
# @return [integer, nil]
|
73
|
+
def event_global_position(event)
|
74
|
+
event&.link&.global_position || event&.global_position
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param sql_builder [PgEventstore::SQLBuilder]
|
78
|
+
# @return [Integer]
|
79
|
+
def estimate_count(sql_builder)
|
80
|
+
sql, params = sql_builder.to_exec_params
|
81
|
+
connection.with do |conn|
|
82
|
+
conn.exec_params("EXPLAIN #{sql}", params)
|
83
|
+
end.to_a.first['QUERY PLAN'].match(/rows=(\d+)/)[1].to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param sql_builder [PgEventstore::SQLBuilder]
|
87
|
+
# @return [Integer]
|
88
|
+
def regular_count(sql_builder)
|
89
|
+
sql_builder.unselect.select('count(*) as count_all')
|
90
|
+
|
91
|
+
connection.with do |conn|
|
92
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
93
|
+
end.to_a.first['count_all']
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Integer, nil]
|
97
|
+
def global_position(sql_builder)
|
98
|
+
connection.with do |conn|
|
99
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
100
|
+
end.to_a.dig(0, 'global_position')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Web
|
5
|
+
module Paginator
|
6
|
+
module Helpers
|
7
|
+
# @param collection [PgEventstore::Paginator::BaseCollection]
|
8
|
+
# @return [String]
|
9
|
+
def previous_page_link(collection)
|
10
|
+
id = collection.prev_page_starting_id
|
11
|
+
disabled = id ? '' : 'disabled'
|
12
|
+
<<~HTML
|
13
|
+
<li class="page-item #{disabled}">
|
14
|
+
<a class="page-link" href="#{build_starting_id_link(id)}" tabindex="-1">Previous</a>
|
15
|
+
</li>
|
16
|
+
HTML
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param collection [PgEventstore::Paginator::BaseCollection]
|
20
|
+
# @return [String]
|
21
|
+
def next_page_link(collection)
|
22
|
+
id = collection.next_page_starting_id
|
23
|
+
disabled = id ? '' : 'disabled'
|
24
|
+
<<~HTML
|
25
|
+
<li class="page-item #{disabled}">
|
26
|
+
<a class="page-link" href="#{build_starting_id_link(id)}" tabindex="-1">Next</a>
|
27
|
+
</li>
|
28
|
+
HTML
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [String]
|
32
|
+
def first_page_link
|
33
|
+
path = build_path(params.slice(*(params.keys - ['starting_id'])))
|
34
|
+
<<~HTML
|
35
|
+
<li class="page-item">
|
36
|
+
<a class="page-link" href="#{path}" tabindex="-1">First</a>
|
37
|
+
</li>
|
38
|
+
HTML
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param per_page [String] string representation of items per page. E.g. "10", "20", etc.
|
42
|
+
# @return [String]
|
43
|
+
def per_page_url(per_page)
|
44
|
+
build_path(params.merge(per_page: per_page))
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param order [String] "asc"/"desc"
|
48
|
+
# @return [String]
|
49
|
+
def sort_url(order)
|
50
|
+
build_path(params.merge(order: order))
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param number [Integer] total number of events by the current filter
|
54
|
+
# @return [String]
|
55
|
+
def total_count(number)
|
56
|
+
prefix =
|
57
|
+
if number > Paginator::EventsCollection::MAX_NUMBER_TO_COUNT
|
58
|
+
"Estimate count: "
|
59
|
+
else
|
60
|
+
"Total count: "
|
61
|
+
end
|
62
|
+
number = number_with_delimiter(number)
|
63
|
+
prefix + number
|
64
|
+
end
|
65
|
+
|
66
|
+
# Takes an integer and adds delimiters in there. E.g 1002341 becomes this "1,002,341"
|
67
|
+
# @param number [Integer]
|
68
|
+
# @param delimiter [String]
|
69
|
+
# @return [String] number with delimiters
|
70
|
+
def number_with_delimiter(number, delimiter: ',')
|
71
|
+
number = number.to_s
|
72
|
+
symbols_to_skip = number.size % 3
|
73
|
+
parts = []
|
74
|
+
parts.push(number[0...symbols_to_skip]) unless symbols_to_skip.zero?
|
75
|
+
parts.push(*number[symbols_to_skip..].scan(/\d{3}/))
|
76
|
+
parts.join(delimiter)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param event [PgEventstore::Event]
|
80
|
+
# @return [String]
|
81
|
+
def stream_path(event)
|
82
|
+
build_path(
|
83
|
+
{
|
84
|
+
filter: {
|
85
|
+
streams: [
|
86
|
+
{
|
87
|
+
context: event.stream.context,
|
88
|
+
stream_name: event.stream.stream_name,
|
89
|
+
stream_id: event.stream.stream_id
|
90
|
+
}
|
91
|
+
]
|
92
|
+
}
|
93
|
+
}
|
94
|
+
)
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# @param starting_id [String, Integer, nil]
|
101
|
+
# @param [String, nil]
|
102
|
+
def build_starting_id_link(starting_id)
|
103
|
+
return 'javascript: void(0);' unless starting_id
|
104
|
+
|
105
|
+
build_path(params.merge(starting_id: starting_id))
|
106
|
+
end
|
107
|
+
|
108
|
+
# @param params [Hash, Array]
|
109
|
+
# @return [String]
|
110
|
+
def build_path(params)
|
111
|
+
encoded_params = Rack::Utils.build_nested_query(params)
|
112
|
+
return request.path if encoded_params.empty?
|
113
|
+
|
114
|
+
"#{request.path}?#{encoded_params}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Web
|
5
|
+
module Paginator
|
6
|
+
class StreamContextsCollection < BaseCollection
|
7
|
+
PER_PAGE = 10
|
8
|
+
|
9
|
+
# @return [Array<Hash>]
|
10
|
+
def collection
|
11
|
+
@_collection ||=
|
12
|
+
begin
|
13
|
+
sql_builder =
|
14
|
+
SQLBuilder.new.select('context').from('partitions').where('stream_name is null and event_type is null')
|
15
|
+
.limit(per_page).order("context #{order}")
|
16
|
+
sql_builder.where("context #{direction_operator} ?", starting_id) if starting_id
|
17
|
+
sql_builder.where('context like ?', "#{options[:query]}%")
|
18
|
+
connection.with do |conn|
|
19
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
20
|
+
end.to_a
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String, nil]
|
25
|
+
def next_page_starting_id
|
26
|
+
return unless collection.size == per_page
|
27
|
+
|
28
|
+
starting_id = collection.first['context']
|
29
|
+
sql_builder =
|
30
|
+
SQLBuilder.new.select('context').from('partitions').where('stream_name is null and event_type is null').
|
31
|
+
where("context #{direction_operator} ?", starting_id).where('context like ?', "#{options[:query]}%").
|
32
|
+
limit(1).offset(per_page).order("context #{order}")
|
33
|
+
|
34
|
+
connection.with do |conn|
|
35
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
36
|
+
end.to_a.dig(0, 'context')
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @return [String]
|
42
|
+
def direction_operator
|
43
|
+
order == :asc ? '>=' : '<='
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Web
|
5
|
+
module Paginator
|
6
|
+
class StreamIdsCollection < BaseCollection
|
7
|
+
PER_PAGE = 10
|
8
|
+
|
9
|
+
# @return [Array<Hash>]
|
10
|
+
def collection
|
11
|
+
@_collection ||=
|
12
|
+
begin
|
13
|
+
sql_builder =
|
14
|
+
SQLBuilder.new.select('stream_id').from('events').
|
15
|
+
where('context = ? and stream_name = ?', options[:context], options[:stream_name]).
|
16
|
+
where('stream_id like ?', "#{options[:query]}%")
|
17
|
+
sql_builder.where("stream_id #{direction_operator} ?", starting_id) if starting_id
|
18
|
+
sql_builder.group('stream_id').limit(per_page).order("stream_id #{order}")
|
19
|
+
connection.with do |conn|
|
20
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
21
|
+
end.to_a
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String, nil]
|
26
|
+
def next_page_starting_id
|
27
|
+
return unless collection.size == per_page
|
28
|
+
|
29
|
+
starting_id = collection.first['stream_id']
|
30
|
+
sql_builder =
|
31
|
+
SQLBuilder.new.select('stream_id').from('events').
|
32
|
+
where("stream_id #{direction_operator} ?", starting_id).where('stream_id like ?', "#{options[:query]}%").
|
33
|
+
where('context = ? and stream_name = ?', options[:context], options[:stream_name]).
|
34
|
+
group('stream_id').limit(1).offset(per_page).order("stream_id #{order}")
|
35
|
+
|
36
|
+
connection.with do |conn|
|
37
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
38
|
+
end.to_a.dig(0, 'stream_id')
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @return [String]
|
44
|
+
def direction_operator
|
45
|
+
order == :asc ? '>=' : '<='
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|