pg_eventstore 0.10.2 → 1.0.0.rc1
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 +6 -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/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 +4 -0
- data/lib/pg_eventstore/queries/event_queries.rb +27 -6
- data/lib/pg_eventstore/queries/links_resolver.rb +28 -6
- 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 +58 -28
- 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/query_builders/events_filtering_query.rb +4 -4
- 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/subscription.rb +28 -12
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +19 -15
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +1 -1
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +1 -1
- 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 +60 -2
|
@@ -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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgEventstore
|
|
4
|
+
module Web
|
|
5
|
+
module Paginator
|
|
6
|
+
class StreamNamesCollection < 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_name').from('partitions').
|
|
15
|
+
where('event_type is null and context = ?', options[:context]).
|
|
16
|
+
where('stream_name like ?', "#{options[:query]}%")
|
|
17
|
+
sql_builder.where("stream_name #{direction_operator} ?", starting_id) if starting_id
|
|
18
|
+
sql_builder.limit(per_page).order("stream_name #{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_name']
|
|
30
|
+
sql_builder =
|
|
31
|
+
SQLBuilder.new.select('stream_name').from('partitions').
|
|
32
|
+
where("stream_name #{direction_operator} ?", starting_id).
|
|
33
|
+
where('stream_name like ?', "#{options[:query]}%").
|
|
34
|
+
where('event_type is null and context = ?', options[:context]).
|
|
35
|
+
limit(1).offset(per_page).order("stream_name #{order}")
|
|
36
|
+
|
|
37
|
+
connection.with do |conn|
|
|
38
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
|
39
|
+
end.to_a.dig(0, 'stream_name')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @return [String]
|
|
45
|
+
def direction_operator
|
|
46
|
+
order == :asc ? '>=' : '<='
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
Binary file
|
|
Binary file
|