pg_eventstore 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +49 -0
  4. data/db/migrations/7_support_reading_streams_system_stream.sql +2 -0
  5. data/docs/reading_events.md +16 -1
  6. data/docs/subscriptions.md +14 -56
  7. data/exe/pg-eventstore +16 -0
  8. data/lib/pg_eventstore/callbacks.rb +16 -0
  9. data/lib/pg_eventstore/cli/commands/base_command.rb +45 -0
  10. data/lib/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rb +38 -0
  11. data/lib/pg_eventstore/cli/commands/help_command.rb +22 -0
  12. data/lib/pg_eventstore/cli/commands/start_subscriptions_command.rb +96 -0
  13. data/lib/pg_eventstore/cli/commands/stop_subscriptions_command.rb +22 -0
  14. data/lib/pg_eventstore/cli/commands.rb +6 -0
  15. data/lib/pg_eventstore/cli/exit_codes.rb +12 -0
  16. data/lib/pg_eventstore/cli/parser_options/base_options.rb +46 -0
  17. data/lib/pg_eventstore/cli/parser_options/default_options.rb +10 -0
  18. data/lib/pg_eventstore/cli/parser_options/metadata.rb +34 -0
  19. data/lib/pg_eventstore/cli/parser_options/subscription_options.rb +31 -0
  20. data/lib/pg_eventstore/cli/parser_options.rb +6 -0
  21. data/lib/pg_eventstore/cli/parsers/base_parser.rb +33 -0
  22. data/lib/pg_eventstore/cli/parsers/default_parser.rb +24 -0
  23. data/lib/pg_eventstore/cli/parsers/subscription_parser.rb +24 -0
  24. data/lib/pg_eventstore/cli/parsers.rb +5 -0
  25. data/lib/pg_eventstore/cli/try_to_delete_subscriptions_set.rb +75 -0
  26. data/lib/pg_eventstore/cli/try_unlock_subscriptions_set.rb +16 -0
  27. data/lib/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rb +67 -0
  28. data/lib/pg_eventstore/cli.rb +42 -0
  29. data/lib/pg_eventstore/commands/read.rb +1 -1
  30. data/lib/pg_eventstore/extensions/options_extension.rb +53 -8
  31. data/lib/pg_eventstore/queries/event_queries.rb +1 -10
  32. data/lib/pg_eventstore/query_builders/events_filtering.rb +27 -0
  33. data/lib/pg_eventstore/rspec/has_option_matcher.rb +42 -41
  34. data/lib/pg_eventstore/stream.rb +8 -0
  35. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +13 -1
  36. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +1 -1
  37. data/lib/pg_eventstore/subscriptions/queries/subscription_command_queries.rb +1 -1
  38. data/lib/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rb +3 -1
  39. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +10 -50
  40. data/lib/pg_eventstore/subscriptions/subscription_feeder_commands/ping.rb +22 -0
  41. data/lib/pg_eventstore/subscriptions/subscription_feeder_commands.rb +1 -0
  42. data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +7 -2
  43. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +44 -10
  44. data/lib/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rb +1 -1
  45. data/lib/pg_eventstore/utils.rb +32 -0
  46. data/lib/pg_eventstore/version.rb +1 -1
  47. data/lib/pg_eventstore/web/application.rb +12 -2
  48. data/lib/pg_eventstore/web/paginator/events_collection.rb +18 -5
  49. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +24 -2
  50. data/lib/pg_eventstore/web/views/home/dashboard.erb +9 -0
  51. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +1 -1
  52. data/lib/pg_eventstore/web/views/home/partials/events.erb +9 -6
  53. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +3 -3
  54. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +15 -0
  55. data/lib/pg_eventstore/web/views/layouts/application.erb +3 -3
  56. data/lib/pg_eventstore/web/views/subscriptions/index.erb +6 -6
  57. data/lib/pg_eventstore.rb +5 -2
  58. data/sig/pg_eventstore/callbacks.rbs +4 -0
  59. data/sig/pg_eventstore/cli/commands/base_command.rbs +13 -0
  60. data/sig/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rbs +14 -0
  61. data/sig/pg_eventstore/cli/commands/help_command.rbs +13 -0
  62. data/sig/pg_eventstore/cli/commands/start_subscriptions_command.rbs +28 -0
  63. data/sig/pg_eventstore/cli/commands/stop_subscriptions_command.rbs +11 -0
  64. data/sig/pg_eventstore/cli/exit_codes.rbs +8 -0
  65. data/sig/pg_eventstore/cli/parser_options/base_options.rbs +15 -0
  66. data/sig/pg_eventstore/cli/parser_options/default_options.rbs +8 -0
  67. data/sig/pg_eventstore/cli/parser_options/metadata.rbs +11 -0
  68. data/sig/pg_eventstore/cli/parser_options/subscription_options.rbs +9 -0
  69. data/sig/pg_eventstore/cli/parsers/base_parser.rbs +18 -0
  70. data/sig/pg_eventstore/cli/parsers/default_parser.rbs +9 -0
  71. data/sig/pg_eventstore/cli/parsers/subscription_parser.rbs +9 -0
  72. data/sig/pg_eventstore/cli/try_to_delete_subscriptions_set.rbs +24 -0
  73. data/sig/pg_eventstore/cli/try_unlock_subscriptions_set.rbs +7 -0
  74. data/sig/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rbs +26 -0
  75. data/sig/pg_eventstore/cli.rbs +10 -0
  76. data/sig/pg_eventstore/extensions/options_extension.rbs +18 -1
  77. data/sig/pg_eventstore/queries/event_queries.rbs +0 -5
  78. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +6 -0
  79. data/sig/pg_eventstore/stream.rbs +3 -0
  80. data/sig/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rbs +8 -0
  81. data/sig/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rbs +7 -1
  82. data/sig/pg_eventstore/subscriptions/queries/subscription_command_queries.rbs +1 -1
  83. data/sig/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rbs +1 -1
  84. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +10 -11
  85. data/sig/pg_eventstore/subscriptions/subscription_feeder_commands/ping.rbs +11 -0
  86. data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +1 -1
  87. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +13 -1
  88. data/sig/pg_eventstore/utils.rbs +2 -0
  89. data/sig/pg_eventstore/web/application.rbs +27 -0
  90. data/sig/pg_eventstore/web/paginator/base_collection.rbs +0 -9
  91. data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +9 -0
  92. data/sig/pg_eventstore/web/paginator/events_collection.rbs +3 -1
  93. data/sig/pg_eventstore.rbs +2 -8
  94. metadata +47 -3
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Parsers
6
+ class BaseParser
7
+ class << self
8
+ # @return [String]
9
+ def banner
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+
14
+ attr_reader :args, :options
15
+
16
+ # @param args [Array<String>]
17
+ # @param options [PgEventstore::CLI::ParserOptions::BaseOptions]
18
+ def initialize(args, options)
19
+ @args = args
20
+ @options = options
21
+ @parser = ::OptionParser.new(self.class.banner)
22
+ end
23
+
24
+ # @return [Array<Array<String>, PgEventstore::CLI::ParserOptions::BaseOptions>] list of commands and parsed
25
+ # options
26
+ def parse
27
+ @options.attach_parser_handlers(@parser)
28
+ [@parser.parse(args), @options]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Parsers
6
+ class DefaultParser < BaseParser
7
+ class << self
8
+ # @return [String]
9
+ def banner
10
+ <<~TEXT
11
+ Usage: pg-eventstore [options]
12
+ pg-eventstore [command]
13
+
14
+ Commands:
15
+ subscriptions Start, stop subscriptions
16
+
17
+ Options:
18
+ TEXT
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Parsers
6
+ class SubscriptionParser < BaseParser
7
+ class << self
8
+ # @return [String]
9
+ def banner
10
+ <<~TEXT
11
+ Usage: pg-eventstore subscriptions [command] [options]
12
+
13
+ Commands:
14
+ start Start subscriptions. Example: pg-eventstore subscriptions start -r lib/my_subscriptions.rb
15
+ stop Stop subscriptions. Example: pg-eventstore subscriptions stop
16
+
17
+ Options:
18
+ TEXT
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parsers/base_parser'
4
+ require_relative 'parsers/default_parser'
5
+ require_relative 'parsers/subscription_parser'
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ class TryToDeleteSubscriptionsSet
6
+ class << self
7
+ def try_to_delete(...)
8
+ new(...).try_to_delete
9
+ end
10
+ end
11
+
12
+ attr_reader :config_name, :subscriptions_set_id
13
+
14
+ # @param config_name [Symbol]
15
+ # @param subscriptions_set_id [Integer]
16
+ def initialize(config_name, subscriptions_set_id)
17
+ @config_name = config_name
18
+ @subscriptions_set_id = subscriptions_set_id
19
+ end
20
+
21
+ # @return [Boolean] whether subscription was deleted
22
+ def try_to_delete
23
+ PgEventstore.logger&.info(
24
+ "Trying to delete SubscriptionsSet##{subscriptions_set_id}."
25
+ )
26
+ cmd_name = SubscriptionFeederCommands.command_class('Ping').new.name
27
+ subscriptions_set_commands_queries.find_or_create_by(
28
+ subscriptions_set_id: subscriptions_set_id, command_name: cmd_name, data: {}
29
+ )
30
+ # Potentially CommandsHandler can be dead exactly at the same moment we expect it to process "Ping" command.
31
+ # Wait for potential recover plus run interval and plus another second to allow potential processing of
32
+ # "Ping" command. "Ping" command comes in prio, so it is guaranteed it will be processed as a first command.
33
+ sleep CommandsHandler::RESTART_DELAY + CommandsHandler::PULL_INTERVAL + 1
34
+ if subscriptions_set_commands_queries.find_by(subscriptions_set_id: subscriptions_set_id, command_name: cmd_name)
35
+ # "Ping" command wasn't consumed. Related process must be dead.
36
+ subscriptions_set_queries.delete(subscriptions_set_id)
37
+ PgEventstore.logger&.info(
38
+ "SubscriptionsSet##{subscriptions_set_id} was deleted successfully. Proceeding with startup process."
39
+ )
40
+ return true
41
+ end
42
+
43
+ PgEventstore.logger&.warn(
44
+ "Failed to delete SubscriptionsSet##{subscriptions_set_id}. It looks alive."
45
+ )
46
+ false
47
+ rescue RecordNotFound
48
+ # Failed to create "Ping" command because related SubscriptionsSet does not exist.
49
+ true
50
+ end
51
+
52
+ private
53
+
54
+ # @return [PgEventstore::SubscriptionsSetQueries]
55
+ def subscriptions_set_queries
56
+ SubscriptionsSetQueries.new(connection)
57
+ end
58
+
59
+ # @return [PgEventstore::SubscriptionsSetCommandQueries]
60
+ def subscriptions_set_commands_queries
61
+ SubscriptionsSetCommandQueries.new(connection)
62
+ end
63
+
64
+ # @return [PgEventstore::Connection]
65
+ def connection
66
+ PgEventstore.connection(config_name)
67
+ end
68
+
69
+ # @return [PgEventstore::Config]
70
+ def config
71
+ PgEventstore.config(config_name)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'try_to_delete_subscriptions_set'
4
+ require_relative 'wait_for_subscriptions_set_shutdown'
5
+
6
+ module PgEventstore
7
+ module CLI
8
+ class TryUnlockSubscriptionsSet
9
+ class << self
10
+ def try_unlock(...)
11
+ TryToDeleteSubscriptionsSet.try_to_delete(...) || WaitForSubscriptionsSetShutdown.wait_for_shutdown(...)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ class WaitForSubscriptionsSetShutdown
6
+ class << self
7
+ def wait_for_shutdown(...)
8
+ new(...).wait_for_shutdown
9
+ end
10
+ end
11
+
12
+ # @return [Float] seconds
13
+ SHUTDOWN_CHECK_INTERVAL = 2.0
14
+
15
+ attr_reader :config_name, :subscriptions_set_id
16
+
17
+ # @param config_name [Symbol]
18
+ # @param subscriptions_set_id [Integer]
19
+ def initialize(config_name, subscriptions_set_id)
20
+ @config_name = config_name
21
+ @subscriptions_set_id = subscriptions_set_id
22
+ end
23
+
24
+ # @return [Boolean]
25
+ def wait_for_shutdown
26
+ PgEventstore.logger&.info(
27
+ "Trying to wait for shutdown of SubscriptionsSet##{subscriptions_set_id}."
28
+ )
29
+ deadline = Time.now.utc + config.subscription_graceful_shutdown_timeout
30
+ loop do
31
+ find_set!
32
+ return false if Time.now.utc > deadline
33
+
34
+ sleep SHUTDOWN_CHECK_INTERVAL
35
+ end
36
+ rescue RecordNotFound
37
+ PgEventstore.logger&.warn(
38
+ "SubscriptionsSet##{subscriptions_set_id} no longer exists. Proceeding with startup process."
39
+ )
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ # @return [PgEventstore::SubscriptionsSetQueries]
46
+ def subscriptions_set_queries
47
+ SubscriptionsSetQueries.new(connection)
48
+ end
49
+
50
+ # @return [PgEventstore::SubscriptionsSet]
51
+ # @raise [PgEventstore::RecordNotFound]
52
+ def find_set!
53
+ SubscriptionsSet.using_connection(config_name).new(**subscriptions_set_queries.find!(subscriptions_set_id))
54
+ end
55
+
56
+ # @return [PgEventstore::Connection]
57
+ def connection
58
+ PgEventstore.connection(config_name)
59
+ end
60
+
61
+ # @return [PgEventstore::Config]
62
+ def config
63
+ PgEventstore.config(config_name)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require_relative 'cli/parsers'
5
+ require_relative 'cli/parser_options'
6
+ require_relative 'cli/try_unlock_subscriptions_set'
7
+ require_relative 'cli/exit_codes'
8
+ require_relative 'cli/commands'
9
+
10
+ module PgEventstore
11
+ module CLI
12
+ OPTIONS_PARSER = {
13
+ "subscriptions" => [Parsers::SubscriptionParser, ParserOptions::SubscriptionOptions].freeze
14
+ }.tap do |directions|
15
+ directions.default = [Parsers::DefaultParser, ParserOptions::DefaultOptions].freeze
16
+ end.freeze
17
+
18
+ COMMANDS = {
19
+ ["subscriptions", "start"].freeze => Commands::StartSubscriptionsCommand,
20
+ ["subscriptions", "stop"].freeze => Commands::StopSubscriptionsCommand
21
+ }.freeze
22
+
23
+ class << self
24
+ # @return [PgEventstore::Callbacks]
25
+ def callbacks
26
+ @callbacks ||= Callbacks.new
27
+ end
28
+
29
+ # @param args [Array<String>]
30
+ # @return [Integer] exit code
31
+ def execute(args)
32
+ options_parser_class, options_class = OPTIONS_PARSER[args[0]]
33
+ command, parsed_options = options_parser_class.new(ARGV, options_class.new).parse
34
+ return Commands::HelpCommand.new(parsed_options).call if parsed_options.help
35
+ return COMMANDS[command].new(parsed_options).call if COMMANDS[command]
36
+
37
+ _, parsed_options = options_parser_class.new(['-h'], options_class.new).parse
38
+ Commands::HelpCommand.new(parsed_options).call
39
+ end
40
+ end
41
+ end
42
+ end
@@ -15,7 +15,7 @@ module PgEventstore
15
15
  # @return [Array<PgEventstore::Event>]
16
16
  # @raise [PgEventstore::StreamNotFoundError]
17
17
  def call(stream, options: {})
18
- queries.events.stream_revision(stream) || raise(StreamNotFoundError, stream) unless stream.all_stream?
18
+ queries.events.stream_revision(stream) || raise(StreamNotFoundError, stream) unless stream.system?
19
19
 
20
20
  queries.events.stream_events(stream, options)
21
21
  end
@@ -40,14 +40,59 @@ module PgEventstore
40
40
  #
41
41
  # SomeClass.new(attr1: 'hihi', attr4: 'byebye')
42
42
  module OptionsExtension
43
+ class Option
44
+ attr_reader :name, :metadata
45
+
46
+ # @param name [Symbol]
47
+ # @param metadata [Object, nil]
48
+ def initialize(name, metadata: nil)
49
+ @name = name
50
+ @metadata = metadata
51
+ end
52
+
53
+ # @param other_option [Object]
54
+ # @return [Boolean]
55
+ def ==(other_option)
56
+ return false unless other_option.is_a?(Option)
57
+
58
+ name == other_option.name
59
+ end
60
+
61
+ # @param other_option [Object]
62
+ # @return [Boolean]
63
+ def eql?(other_option)
64
+ return false unless other_option.is_a?(Option)
65
+
66
+ name.eql?(other_option.name)
67
+ end
68
+
69
+ # @return [Integer]
70
+ def hash
71
+ name.hash
72
+ end
73
+ end
74
+
75
+ class Options < Set
76
+ def add(o)
77
+ @hash[o] = o
78
+ self
79
+ end
80
+
81
+ # @param option [Symbol]
82
+ # @return [PgEventstore::Extensions::OptionsExtension::Option, nil]
83
+ def [](option)
84
+ @hash[Option.new(option)]
85
+ end
86
+ end
87
+
43
88
  # @!visibility private
44
89
  module ClassMethods
45
90
  # @param opt_name [Symbol] option name
46
91
  # @param blk [Proc] provide define value using block. It will be later evaluated in the
47
92
  # context of your object to determine the default value of the option
48
93
  # @return [Symbol]
49
- def option(opt_name, &blk)
50
- self.options = (options + Set.new([opt_name])).freeze
94
+ def option(opt_name, metadata: nil, &blk)
95
+ self.options = (options + Options.new([Option.new(opt_name, metadata: metadata)])).freeze
51
96
  warn_already_defined(opt_name)
52
97
  warn_already_defined(:"#{opt_name}=")
53
98
  define_method "#{opt_name}=" do |value|
@@ -67,7 +112,7 @@ module PgEventstore
67
112
 
68
113
  def inherited(klass)
69
114
  super
70
- klass.options = Set.new(options).freeze
115
+ klass.options = Options.new(options).freeze
71
116
  end
72
117
 
73
118
  private
@@ -86,7 +131,7 @@ module PgEventstore
86
131
 
87
132
  def self.included(klass)
88
133
  klass.singleton_class.attr_accessor(:options)
89
- klass.options = Set.new.freeze
134
+ klass.options = Options.new.freeze
90
135
  klass.extend(ClassMethods)
91
136
  end
92
137
 
@@ -100,7 +145,7 @@ module PgEventstore
100
145
  # @return [Hash]
101
146
  def options_hash
102
147
  self.class.options.each_with_object({}) do |option, res|
103
- res[option] = public_send(option)
148
+ res[option.name] = public_send(option.name)
104
149
  end
105
150
  end
106
151
  alias attributes_hash options_hash
@@ -108,7 +153,7 @@ module PgEventstore
108
153
  # @param opt_name [Symbol]
109
154
  # @return [Boolean]
110
155
  def readonly!(opt_name)
111
- return false unless self.class.options.include?(opt_name)
156
+ return false unless self.class.options.include?(Option.new(opt_name))
112
157
 
113
158
  @readonly.add(opt_name)
114
159
  true
@@ -136,8 +181,8 @@ module PgEventstore
136
181
  def init_default_values(options)
137
182
  self.class.options.each do |option|
138
183
  # init default values of options
139
- value = options.key?(option) ? options[option] : public_send(option)
140
- public_send("#{option}=", value)
184
+ value = options.key?(option.name) ? options[option.name] : public_send(option.name)
185
+ public_send("#{option.name}=", value)
141
186
  end
142
187
  end
143
188
  end
@@ -70,7 +70,7 @@ module PgEventstore
70
70
  # @param options [Hash]
71
71
  # @return [Array<PgEventstore::Event>]
72
72
  def stream_events(stream, options)
73
- exec_params = events_filtering(stream, options).to_exec_params
73
+ exec_params = QueryBuilders::EventsFiltering.events_filtering(stream, options).to_exec_params
74
74
  raw_events = connection.with do |conn|
75
75
  conn.exec_params(*exec_params)
76
76
  end.to_a
@@ -128,15 +128,6 @@ module PgEventstore
128
128
  [sql_rows_for_insert, values]
129
129
  end
130
130
 
131
- # @param stream [PgEventstore::Stream]
132
- # @param options [Hash]
133
- # @return [PgEventstore::EventsFiltering]
134
- def events_filtering(stream, options)
135
- return QueryBuilders::EventsFiltering.all_stream_filtering(options) if stream.all_stream?
136
-
137
- QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options)
138
- end
139
-
140
131
  # @return [PgEventstore::LinksResolver]
141
132
  def links_resolver
142
133
  LinksResolver.new(connection)
@@ -21,6 +21,18 @@ module PgEventstore
21
21
  SUBSCRIPTIONS_OPTIONS = %i[from_position resolve_link_tos filter max_count].freeze
22
22
 
23
23
  class << self
24
+ # @param stream [PgEventstore::Stream]
25
+ # @param options [Hash]
26
+ # @return [PgEventstore::EventsFiltering]
27
+ def events_filtering(stream, options)
28
+ return all_stream_filtering(options) if stream.all_stream?
29
+ if stream.system? && Stream::KNOWN_SYSTEM_STREAMS.include?(stream.context)
30
+ return system_stream_filtering(stream, options)
31
+ end
32
+
33
+ specific_stream_filtering(stream, options)
34
+ end
35
+
24
36
  # @param options [Hash]
25
37
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
26
38
  def subscriptions_events_filtering(options)
@@ -54,6 +66,15 @@ module PgEventstore
54
66
  event_filter.add_stream_direction(options[:direction])
55
67
  event_filter
56
68
  end
69
+
70
+ # @param stream [PgEventstore::Stream] system stream
71
+ # @param options [Hash]
72
+ # @return [PgEventstore::QueryBuilders::EventsFiltering]
73
+ def system_stream_filtering(stream, options)
74
+ all_stream_filtering(options).tap do |event_filter|
75
+ event_filter.set_source(stream.context)
76
+ end
77
+ end
57
78
  end
58
79
 
59
80
  def initialize
@@ -139,6 +160,12 @@ module PgEventstore
139
160
  @sql_builder.to_exec_params
140
161
  end
141
162
 
163
+ # @param table_name [String] system stream view name
164
+ # @return [void]
165
+ def set_source(table_name)
166
+ @sql_builder.from(%{ "#{PG::Connection.escape(table_name)}" events })
167
+ end
168
+
142
169
  private
143
170
 
144
171
  # @param stream_attrs [Hash]
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # This matcher is defined to test options which are defined by using
4
- # EventStoreClient::Extensions::OptionsExtension option. Example:
4
+ # PgEventstore::Extensions::OptionsExtension option. Example:
5
5
  # Let's say you have next class
6
6
  # class SomeClass
7
7
  # include PgEventstore::Extensions::OptionsExtension
8
8
  #
9
- # option(:some_opt) { '1' }
9
+ # option(:some_opt, metadata: { foo: :bar }) { '1' }
10
10
  # end
11
11
  #
12
- # To test that its instance has the proper option with the proper default value you can use this
12
+ # To test that its instance has the proper option with the proper default value and proper metadata you can use this
13
13
  # matcher:
14
14
  # RSpec.describe SomeClass do
15
15
  # subject { described_class.new }
@@ -17,53 +17,50 @@
17
17
  # # Check that :some_opt is present
18
18
  # it { is_expected.to have_option(:some_opt) }
19
19
  # # Check that :some_opt is present and has the correct default value
20
- # it { is_expected.to have_option(:some_opt).with_default_value('1') }
21
- # end
22
- #
23
- # If you have more complex implementation of default value of your option - you should handle it
24
- # customly. For example:
25
- # class SomeClass
26
- # include PgEventstore::Extensions::OptionsExtension
27
- #
28
- # option(:some_opt) { calc_value }
29
- # end
30
- # You could test it like so:
31
- # RSpec.described SomeClass do
32
- # let(:instance) { described_class.new }
33
- #
34
- # describe ':some_opt default value' do
35
- # subject { instance.some_opt }
36
- #
37
- # let(:value) { 'some val' }
38
- #
39
- # before do
40
- # allow(instance).to receive(:calc_value).and_return(value)
41
- # end
42
- #
43
- # it { is_expected.to eq(value) }
44
- # end
20
+ # it { is_expected.to have_option(:some_opt).with_default_value('1').with_metadata(foo: :bar) }
45
21
  # end
46
22
  RSpec::Matchers.define :has_option do |option_name|
47
23
  match do |obj|
48
- option_presence = obj.class.respond_to?(:options) && obj.class.options.include?(option_name)
49
- if @default_value
50
- option_presence && RSpec::Matchers::BuiltIn::Match.new(@default_value).matches?(obj.class.allocate.public_send(option_name))
51
- else
52
- option_presence
24
+ option = obj.class.options[option_name]
25
+ is_correct = obj.class.respond_to?(:options) && option
26
+ if defined?(@default_value)
27
+ is_correct &&=
28
+ RSpec::Matchers::BuiltIn::Match.new(@default_value).matches?(obj.class.allocate.public_send(option_name))
53
29
  end
30
+ if defined?(@metadata)
31
+ is_correct &&= RSpec::Matchers::BuiltIn::Match.new(@metadata).matches?(option.metadata)
32
+ end
33
+ is_correct
54
34
  end
55
35
 
56
36
  failure_message do |obj|
57
- option_presence = obj.class.respond_to?(:options) && obj.class.options.include?(option_name)
58
- if option_presence && @default_value
59
- msg = "Expected #{obj.class} to have `#{option_name.inspect}' option with #{@default_value.inspect}"
60
- msg += ' default value, but default value is'
61
- msg += " #{obj.class.allocate.public_send(option_name).inspect}"
37
+ option = obj.class.options[option_name]
38
+ option_presence = obj.class.respond_to?(:options) && option
39
+
40
+ default_value_message = "with default value #{@default_value.inspect}"
41
+ metadata_message = "with metadata #{@metadata.inspect}"
42
+ message = "Expected #{obj.class} to have `#{option_name.inspect}' option"
43
+ message += " #{default_value_message}," if defined?(@default_value)
44
+ message += " #{metadata_message}," if defined?(@metadata)
45
+ message += "," unless defined?(@metadata) || defined?(@default_value)
46
+ if option_presence
47
+ actual_default_value = obj.class.allocate.public_send(option_name)
48
+ actual_metadata = option.metadata
49
+ default_value_matches = RSpec::Matchers::BuiltIn::Match.new(@default_value).matches?(actual_default_value)
50
+ metadata_matches = RSpec::Matchers::BuiltIn::Match.new(@metadata).matches?(actual_metadata)
51
+
52
+ case [default_value_matches, metadata_matches]
53
+ when [false, true]
54
+ message += " but default value is #{actual_default_value.inspect}"
55
+ when [true, false]
56
+ message += " but metadata is #{actual_metadata.inspect}"
57
+ else
58
+ message += " but default value is #{actual_default_value.inspect} and metadata is #{actual_metadata.inspect}"
59
+ end
62
60
  else
63
- msg = "Expected #{obj} to have `#{option_name.inspect}' option."
61
+ message += " but there is no option found with the given name"
64
62
  end
65
-
66
- msg
63
+ message
67
64
  end
68
65
 
69
66
  description do
@@ -83,6 +80,10 @@ RSpec::Matchers.define :has_option do |option_name|
83
80
  chain :with_default_value do |val|
84
81
  @default_value = val
85
82
  end
83
+
84
+ chain :with_metadata do |val|
85
+ @metadata = val
86
+ end
86
87
  end
87
88
 
88
89
  RSpec::Matchers.alias_matcher :have_option, :has_option
@@ -8,6 +8,8 @@ module PgEventstore
8
8
  SYSTEM_STREAM_PREFIX = '$'
9
9
  # @return [Integer]
10
10
  NON_EXISTING_STREAM_REVISION = -1
11
+ # @return [Array<String>]
12
+ KNOWN_SYSTEM_STREAMS = %w[$streams].freeze
11
13
 
12
14
  class << self
13
15
  # Produces "all" stream instance. "all" stream does not represent any specific stream. Instead, it indicates that
@@ -18,6 +20,12 @@ module PgEventstore
18
20
  stream.instance_variable_set(:@all_stream, true)
19
21
  end
20
22
  end
23
+
24
+ # @param name [String]
25
+ # @return [PgEventstore::Stream]
26
+ def system_stream(name)
27
+ new(context: name, stream_name: '', stream_id: '')
28
+ end
21
29
  end
22
30
 
23
31
  # @!attribute context
@@ -13,7 +13,7 @@ module PgEventstore
13
13
  # Look up commands for the given SubscriptionFeeder and execute them
14
14
  # @return [void]
15
15
  def process
16
- queries.find_commands(@subscription_feeder.id).each do |command|
16
+ commands.each do |command|
17
17
  command.exec_cmd(@subscription_feeder)
18
18
  queries.delete(command.id)
19
19
  end
@@ -21,6 +21,18 @@ module PgEventstore
21
21
 
22
22
  private
23
23
 
24
+ # @return [Array<PgEventstore::SubscriptionFeederCommands::Base>]
25
+ def commands
26
+ commands = queries.find_commands(@subscription_feeder.id)
27
+ ping_cmd = commands.find do |cmd|
28
+ cmd.name == 'Ping'
29
+ end
30
+ return commands unless ping_cmd
31
+
32
+ # "Ping" command should go in prio
33
+ [ping_cmd, *(commands - [ping_cmd])]
34
+ end
35
+
24
36
  # @return [PgEventstore::SubscriptionsSetCommandQueries]
25
37
  def queries
26
38
  SubscriptionsSetCommandQueries.new(connection)
@@ -5,7 +5,7 @@ module PgEventstore
5
5
  class SubscriptionRunnersCommands
6
6
  # @param config_name [Symbol]
7
7
  # @param runners [Array<PgEventstore::SubscriptionRunner>]
8
- # @param subscriptions_set_id [Integer]
8
+ # @param subscriptions_set_id [Integer, nil]
9
9
  def initialize(config_name, runners, subscriptions_set_id)
10
10
  @config_name = config_name
11
11
  @runners = runners.to_h { |runner| [runner.id, runner] }
@@ -64,7 +64,7 @@ module PgEventstore
64
64
  end
65
65
 
66
66
  # @param subscription_ids [Array<Integer>]
67
- # @param subscriptions_set_id [Integer]
67
+ # @param subscriptions_set_id [Integer, nil]
68
68
  # @return [Array<PgEventstore::SubscriptionRunnerCommands::Base>]
69
69
  def find_commands(subscription_ids, subscriptions_set_id:)
70
70
  return [] if subscription_ids.empty?