pg_eventstore 0.10.2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +26 -0
  4. data/db/migrations/1_create_events.sql +12 -11
  5. data/db/migrations/2_create_subscriptions.sql +6 -2
  6. data/db/migrations/3_create_subscription_commands.sql +9 -5
  7. data/db/migrations/4_create_subscriptions_set_commands.sql +1 -1
  8. data/db/migrations/5_partitions.sql +1 -0
  9. data/docs/how_it_works.md +14 -1
  10. data/lib/pg_eventstore/commands/append.rb +1 -1
  11. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +30 -8
  12. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +8 -10
  13. data/lib/pg_eventstore/commands/link_to.rb +14 -7
  14. data/lib/pg_eventstore/errors.rb +10 -12
  15. data/lib/pg_eventstore/event.rb +4 -0
  16. data/lib/pg_eventstore/queries/event_queries.rb +27 -6
  17. data/lib/pg_eventstore/queries/links_resolver.rb +28 -6
  18. data/lib/pg_eventstore/queries/partition_queries.rb +8 -0
  19. data/lib/pg_eventstore/queries/subscription_command_queries.rb +27 -7
  20. data/lib/pg_eventstore/queries/subscription_queries.rb +58 -28
  21. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +13 -1
  22. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +18 -4
  23. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +4 -4
  24. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +10 -2
  25. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +9 -7
  26. data/lib/pg_eventstore/subscriptions/commands_handler.rb +3 -2
  27. data/lib/pg_eventstore/subscriptions/subscription.rb +28 -12
  28. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +19 -15
  29. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +1 -1
  30. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +1 -1
  31. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +22 -1
  32. data/lib/pg_eventstore/version.rb +1 -1
  33. data/lib/pg_eventstore/web/application.rb +180 -0
  34. data/lib/pg_eventstore/web/paginator/base_collection.rb +56 -0
  35. data/lib/pg_eventstore/web/paginator/event_types_collection.rb +50 -0
  36. data/lib/pg_eventstore/web/paginator/events_collection.rb +105 -0
  37. data/lib/pg_eventstore/web/paginator/helpers.rb +119 -0
  38. data/lib/pg_eventstore/web/paginator/stream_contexts_collection.rb +48 -0
  39. data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +50 -0
  40. data/lib/pg_eventstore/web/paginator/stream_names_collection.rb +51 -0
  41. data/lib/pg_eventstore/web/public/fonts/vendor/FontAwesome.otf +0 -0
  42. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.eot +0 -0
  43. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.svg +685 -0
  44. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.ttf +0 -0
  45. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.woff +0 -0
  46. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.woff2 +0 -0
  47. data/lib/pg_eventstore/web/public/images/favicon.ico +0 -0
  48. data/lib/pg_eventstore/web/public/javascripts/gentelella.js +334 -0
  49. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +162 -0
  50. data/lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js +7 -0
  51. data/lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js.map +1 -0
  52. data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.autocomplete.min.js +8 -0
  53. data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js +4 -0
  54. data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js.map +1 -0
  55. data/lib/pg_eventstore/web/public/javascripts/vendor/select2.full.min.js +2 -0
  56. data/lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css +5 -0
  57. data/lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css +7 -0
  58. data/lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css.map +1 -0
  59. data/lib/pg_eventstore/web/public/stylesheets/vendor/font-awesome.min.css +4 -0
  60. data/lib/pg_eventstore/web/public/stylesheets/vendor/font-awesome.min.css.map +7 -0
  61. data/lib/pg_eventstore/web/public/stylesheets/vendor/gentelella.min.css +13 -0
  62. data/lib/pg_eventstore/web/public/stylesheets/vendor/select2-bootstrap4.min.css +3 -0
  63. data/lib/pg_eventstore/web/public/stylesheets/vendor/select2.min.css +2 -0
  64. data/lib/pg_eventstore/web/subscriptions/helpers.rb +76 -0
  65. data/lib/pg_eventstore/web/subscriptions/set_collection.rb +34 -0
  66. data/lib/pg_eventstore/web/subscriptions/subscriptions.rb +33 -0
  67. data/lib/pg_eventstore/web/subscriptions/subscriptions_set.rb +33 -0
  68. data/lib/pg_eventstore/web/subscriptions/subscriptions_to_set_association.rb +32 -0
  69. data/lib/pg_eventstore/web/views/home/dashboard.erb +147 -0
  70. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +15 -0
  71. data/lib/pg_eventstore/web/views/home/partials/events.erb +22 -0
  72. data/lib/pg_eventstore/web/views/home/partials/pagination_links.erb +3 -0
  73. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +31 -0
  74. data/lib/pg_eventstore/web/views/layouts/application.erb +116 -0
  75. data/lib/pg_eventstore/web/views/subscriptions/index.erb +220 -0
  76. data/lib/pg_eventstore/web.rb +22 -0
  77. data/lib/pg_eventstore.rb +5 -0
  78. data/pg_eventstore.gemspec +2 -1
  79. 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