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.
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