pg_eventstore 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +2 -0
- data/db/migrations/10_create_subscription_commands.sql +15 -0
- data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
- data/db/migrations/12_improve_events_indexes.sql +1 -0
- data/db/migrations/13_remove_duplicated_index.sql +1 -0
- data/db/migrations/9_create_subscriptions.sql +46 -0
- data/docs/configuration.md +42 -21
- data/docs/linking_events.md +96 -0
- data/docs/reading_events.md +56 -0
- data/docs/subscriptions.md +170 -0
- data/lib/pg_eventstore/callbacks.rb +122 -0
- data/lib/pg_eventstore/client.rb +32 -2
- data/lib/pg_eventstore/commands/append.rb +3 -11
- data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +22 -0
- data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +24 -0
- data/lib/pg_eventstore/commands/link_to.rb +33 -0
- data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +63 -0
- data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +62 -0
- data/lib/pg_eventstore/commands.rb +5 -0
- data/lib/pg_eventstore/config.rb +35 -3
- data/lib/pg_eventstore/errors.rb +80 -0
- data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +10 -22
- data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +69 -29
- data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
- data/lib/pg_eventstore/pg_connection.rb +20 -3
- data/lib/pg_eventstore/queries/event_queries.rb +18 -34
- data/lib/pg_eventstore/queries/event_type_queries.rb +24 -0
- data/lib/pg_eventstore/queries/preloader.rb +37 -0
- data/lib/pg_eventstore/queries/stream_queries.rb +14 -1
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +166 -0
- data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
- data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
- data/lib/pg_eventstore/queries.rb +7 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +17 -22
- data/lib/pg_eventstore/sql_builder.rb +54 -10
- data/lib/pg_eventstore/stream.rb +2 -1
- data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
- data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
- data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
- data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
- data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
- data/lib/pg_eventstore/tasks/setup.rake +5 -1
- data/lib/pg_eventstore/utils.rb +66 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore.rb +19 -1
- metadata +38 -4
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Extensions
|
5
|
+
# Integrates PgEventstore::Calbacks into your object. Example usage:
|
6
|
+
# class MyAwesomeClass
|
7
|
+
# include CallbacksExtension
|
8
|
+
# end
|
9
|
+
# Now you have {#define_callback} public method to define callbacks outside your class' object, and you can use
|
10
|
+
# _#callbacks_ private method to run callbacks inside your class' object. You can also use _.has_callbacks_
|
11
|
+
# public class method to wrap the desired method into {Callbacks#run_callbacks}. Example:
|
12
|
+
# class MyAwesomeClass
|
13
|
+
# include PgEventstore::Extensions::CallbacksExtension
|
14
|
+
#
|
15
|
+
# def initialize(foo)
|
16
|
+
# @foo = foo
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def do_something
|
20
|
+
# puts "I did something useful: #{@foo.inspect}!"
|
21
|
+
# end
|
22
|
+
# has_callbacks :something_happened, :do_something
|
23
|
+
#
|
24
|
+
# def do_something_else
|
25
|
+
# callbacks.run_callbacks(:something_else_happened) do
|
26
|
+
# puts "I did something else!"
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# obj = MyAwesomeClass.new(:foo)
|
32
|
+
# obj.define_callback(
|
33
|
+
# :something_happened, :before, proc { puts "In before callback of :something_happened." }
|
34
|
+
# )
|
35
|
+
# obj.define_callback(
|
36
|
+
# :something_else_happened, :before, proc { puts "In before callback of :something_else_happened." }
|
37
|
+
# )
|
38
|
+
# obj.do_something
|
39
|
+
# obj.do_something_else
|
40
|
+
# Outputs:
|
41
|
+
# In before callback of :something_happened.
|
42
|
+
# I did something useful: :foo!
|
43
|
+
# In before callback of :something_else_happened.
|
44
|
+
# I did something else!
|
45
|
+
module CallbacksExtension
|
46
|
+
def self.included(klass)
|
47
|
+
klass.extend(ClassMethods)
|
48
|
+
klass.prepend(InitCallbacks)
|
49
|
+
klass.class_eval do
|
50
|
+
attr_reader :callbacks
|
51
|
+
private :callbacks
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def define_callback(...)
|
56
|
+
callbacks.define_callback(...)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @!visibility private
|
60
|
+
module InitCallbacks
|
61
|
+
def initialize(...)
|
62
|
+
@callbacks = Callbacks.new
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @!visibility private
|
68
|
+
module ClassMethods
|
69
|
+
# Wraps method with Callbacks#run_callbacks. This allows you to define callbacks by the given action
|
70
|
+
# @param action [String, Symbol]
|
71
|
+
# @param method_name [Symbol]
|
72
|
+
# @return [void]
|
73
|
+
def has_callbacks(action, method_name)
|
74
|
+
visibility_method = visibility_method(method_name)
|
75
|
+
m = Module.new do
|
76
|
+
define_method(method_name) do |*args, **kwargs, &blk|
|
77
|
+
callbacks.run_callbacks(action) { super(*args, **kwargs, &blk) }
|
78
|
+
end
|
79
|
+
send visibility_method, method_name
|
80
|
+
end
|
81
|
+
prepend m
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def visibility_method(method_name)
|
87
|
+
return :public if public_method_defined?(method_name)
|
88
|
+
return :protected if protected_method_defined?(method_name)
|
89
|
+
|
90
|
+
:private
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -7,38 +7,40 @@ module PgEventstore
|
|
7
7
|
# A very simple extension that implements a DSL for adding attr_accessors with default values,
|
8
8
|
# and assigning their values during object initialization.
|
9
9
|
# Example. Let's say you frequently do something like this:
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# attr_accessor :attr1, :attr2, :attr3, :attr4
|
10
|
+
# class SomeClass
|
11
|
+
# attr_accessor :attr1, :attr2, :attr3, :attr4
|
13
12
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
13
|
+
# def initialize(opts = {})
|
14
|
+
# @attr1 = opts[:attr1] || 'Attr 1 value'
|
15
|
+
# @attr2 = opts[:attr2] || 'Attr 2 value'
|
16
|
+
# @attr3 = opts[:attr3] || do_some_calc
|
17
|
+
# @attr4 = opts[:attr4]
|
18
|
+
# end
|
20
19
|
#
|
21
|
-
#
|
20
|
+
# def do_some_calc
|
21
|
+
# "Some calculations"
|
22
|
+
# end
|
22
23
|
# end
|
23
|
-
# end
|
24
24
|
#
|
25
|
-
#
|
26
|
-
# ```
|
25
|
+
# SomeClass.new(attr1: 'hihi', attr4: 'byebye')
|
27
26
|
#
|
28
27
|
# You can replace the code above using the OptionsExtension:
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
28
|
+
# class SomeClass
|
29
|
+
# include PgEventstore::Extensions::OptionsExtension
|
30
|
+
#
|
31
|
+
# option(:attr1) { 'Attr 1 value' }
|
32
|
+
# option(:attr2) { 'Attr 2 value' }
|
33
|
+
# option(:attr3) { do_some_calc }
|
34
|
+
# option(:attr4)
|
32
35
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
# end
|
36
|
+
# def do_some_calc
|
37
|
+
# "Some calculations"
|
38
|
+
# end
|
39
|
+
# end
|
38
40
|
#
|
39
|
-
#
|
40
|
-
# ```
|
41
|
+
# SomeClass.new(attr1: 'hihi', attr4: 'byebye')
|
41
42
|
module OptionsExtension
|
43
|
+
# @!visibility private
|
42
44
|
module ClassMethods
|
43
45
|
# @param opt_name [Symbol] option name
|
44
46
|
# @param blk [Proc] provide define value using block. It will be later evaluated in the
|
@@ -48,7 +50,11 @@ module PgEventstore
|
|
48
50
|
self.options = (options + Set.new([opt_name])).freeze
|
49
51
|
warn_already_defined(opt_name)
|
50
52
|
warn_already_defined(:"#{opt_name}=")
|
51
|
-
|
53
|
+
define_method "#{opt_name}=" do |value|
|
54
|
+
readonly_error(opt_name) if readonly?(opt_name)
|
55
|
+
|
56
|
+
instance_variable_set(:"@#{opt_name}", value)
|
57
|
+
end
|
52
58
|
|
53
59
|
define_method opt_name do
|
54
60
|
result = instance_variable_get(:"@#{opt_name}")
|
@@ -75,6 +81,8 @@ module PgEventstore
|
|
75
81
|
end
|
76
82
|
end
|
77
83
|
|
84
|
+
ReadonlyAttributeError = Class.new(StandardError)
|
85
|
+
|
78
86
|
def self.included(klass)
|
79
87
|
klass.singleton_class.attr_accessor(:options)
|
80
88
|
klass.options = Set.new.freeze
|
@@ -82,11 +90,8 @@ module PgEventstore
|
|
82
90
|
end
|
83
91
|
|
84
92
|
def initialize(**options)
|
85
|
-
|
86
|
-
|
87
|
-
value = options.key?(option) ? options[option] : public_send(option)
|
88
|
-
public_send("#{option}=", value)
|
89
|
-
end
|
93
|
+
@readonly = Set.new
|
94
|
+
init_default_values(options)
|
90
95
|
end
|
91
96
|
|
92
97
|
# Construct a hash from options, where key is the option's name and the value is option's
|
@@ -98,6 +103,41 @@ module PgEventstore
|
|
98
103
|
end
|
99
104
|
end
|
100
105
|
alias attributes_hash options_hash
|
106
|
+
|
107
|
+
# @param opt_name [Symbol]
|
108
|
+
# @return [Boolean]
|
109
|
+
def readonly!(opt_name)
|
110
|
+
return false unless self.class.options.include?(opt_name)
|
111
|
+
|
112
|
+
@readonly.add(opt_name)
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param opt_name [Symbol]
|
117
|
+
# @return [Boolean]
|
118
|
+
def readonly?(opt_name)
|
119
|
+
@readonly.include?(opt_name)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# @param opt_name [Symbol]
|
125
|
+
# @raise [PgEventstore::Extensions::OptionsExtension::ReadOnlyError]
|
126
|
+
def readonly_error(opt_name)
|
127
|
+
raise(
|
128
|
+
ReadonlyAttributeError, "#{opt_name.inspect} attribute was marked as read only. You can no longer modify it."
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
# @param options [Hash]
|
133
|
+
# @return [void]
|
134
|
+
def init_default_values(options)
|
135
|
+
self.class.options.each do |option|
|
136
|
+
# init default values of options
|
137
|
+
value = options.key?(option) ? options[option] : public_send(option)
|
138
|
+
public_send("#{option}=", value)
|
139
|
+
end
|
140
|
+
end
|
101
141
|
end
|
102
142
|
end
|
103
143
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Extensions
|
5
|
+
# Extension that implements creating of a subclass of the class it is used in. The point of creating a subclass is
|
6
|
+
# to bound it to the specific connection. This way the specific connection will be available within tha class and
|
7
|
+
# all its instances without affecting on the original class.
|
8
|
+
# @!visibility private
|
9
|
+
module UsingConnectionExtension
|
10
|
+
def self.included(klass)
|
11
|
+
klass.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def connection
|
16
|
+
raise("No connection was set. Are you trying to manipulate #{name} outside of its lifecycle?")
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param config_name [Symbol]
|
20
|
+
# @return [Class<PgEventstore::Subscription>]
|
21
|
+
def using_connection(config_name)
|
22
|
+
original_class = self
|
23
|
+
Class.new(original_class).tap do |klass|
|
24
|
+
klass.define_singleton_method(:connection) { PgEventstore.connection(config_name) }
|
25
|
+
klass.class_eval do
|
26
|
+
[:to_s, :inspect, :name].each do |m|
|
27
|
+
define_singleton_method(m, &original_class.method(m))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -19,11 +19,28 @@ module PgEventstore
|
|
19
19
|
|
20
20
|
sql = sql.gsub(/\$\d+/).each do |matched|
|
21
21
|
value = params[matched[1..].to_i - 1]
|
22
|
-
|
23
|
-
|
24
|
-
value.is_a?(String) ? "'#{value}'" : value
|
22
|
+
value = encode_value(value)
|
23
|
+
normalize_value(value)
|
25
24
|
end unless params&.empty?
|
26
25
|
PgEventstore.logger.debug(sql)
|
27
26
|
end
|
27
|
+
|
28
|
+
def encode_value(value)
|
29
|
+
encoder = type_map_for_queries[value.class]
|
30
|
+
return type_map_for_queries.send(encoder, value).encode(value) if encoder.is_a?(Symbol)
|
31
|
+
|
32
|
+
type_map_for_queries[value.class]&.encode(value) || value
|
33
|
+
end
|
34
|
+
|
35
|
+
def normalize_value(value)
|
36
|
+
case value
|
37
|
+
when String
|
38
|
+
"'#{value}'"
|
39
|
+
when NilClass
|
40
|
+
'NULL'
|
41
|
+
else
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
28
45
|
end
|
29
46
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'pg_eventstore/query_builders/events_filtering_query'
|
4
|
-
|
5
3
|
module PgEventstore
|
6
4
|
# @!visibility private
|
7
5
|
class EventQueries
|
@@ -10,7 +8,7 @@ module PgEventstore
|
|
10
8
|
|
11
9
|
# @param connection [PgEventstore::Connection]
|
12
10
|
# @param serializer [PgEventstore::EventSerializer]
|
13
|
-
# @param deserializer [PgEventstore::
|
11
|
+
# @param deserializer [PgEventstore::EventDeserializer]
|
14
12
|
def initialize(connection, serializer, deserializer)
|
15
13
|
@connection = connection
|
16
14
|
@serializer = serializer
|
@@ -22,12 +20,13 @@ module PgEventstore
|
|
22
20
|
# @param options [Hash]
|
23
21
|
# @return [Array<PgEventstore::Event>]
|
24
22
|
def stream_events(stream, options)
|
25
|
-
options = include_event_types_ids(options)
|
23
|
+
options = event_type_queries.include_event_types_ids(options)
|
26
24
|
exec_params = events_filtering(stream, options).to_exec_params
|
27
|
-
|
25
|
+
raw_events = connection.with do |conn|
|
28
26
|
conn.exec_params(*exec_params)
|
29
|
-
end
|
30
|
-
|
27
|
+
end.to_a
|
28
|
+
preloader.preload_related_objects(raw_events)
|
29
|
+
deserializer.deserialize_many(raw_events)
|
31
30
|
end
|
32
31
|
|
33
32
|
# @param stream [PgEventstore::Stream] persisted stream
|
@@ -42,14 +41,14 @@ module PgEventstore
|
|
42
41
|
|
43
42
|
sql = <<~SQL
|
44
43
|
INSERT INTO events (#{attributes.keys.join(', ')})
|
45
|
-
VALUES (#{positional_vars(attributes.values)})
|
44
|
+
VALUES (#{Utils.positional_vars(attributes.values)})
|
46
45
|
RETURNING *, $#{attributes.values.size + 1} as type
|
47
46
|
SQL
|
48
47
|
|
49
|
-
|
48
|
+
raw_event = connection.with do |conn|
|
50
49
|
conn.exec_params(sql, [*attributes.values, event.type])
|
51
|
-
end
|
52
|
-
deserializer.without_middlewares.
|
50
|
+
end.to_a.first
|
51
|
+
deserializer.without_middlewares.deserialize(raw_event).tap do |persisted_event|
|
53
52
|
persisted_event.stream = stream
|
54
53
|
end
|
55
54
|
end
|
@@ -58,36 +57,21 @@ module PgEventstore
|
|
58
57
|
|
59
58
|
# @param stream [PgEventstore::Stream]
|
60
59
|
# @param options [Hash]
|
61
|
-
# @param offset [Integer]
|
62
60
|
# @return [PgEventstore::EventsFilteringQuery]
|
63
|
-
def events_filtering(stream, options
|
64
|
-
return QueryBuilders::EventsFiltering.all_stream_filtering(options
|
61
|
+
def events_filtering(stream, options)
|
62
|
+
return QueryBuilders::EventsFiltering.all_stream_filtering(options) if stream.all_stream?
|
65
63
|
|
66
|
-
QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options
|
67
|
-
end
|
68
|
-
|
69
|
-
# Replaces filter by event type strings with filter by event type ids
|
70
|
-
# @param options [Hash]
|
71
|
-
# @return [Hash]
|
72
|
-
def include_event_types_ids(options)
|
73
|
-
options in { filter: { event_types: Array => event_types } }
|
74
|
-
return options unless event_types
|
75
|
-
|
76
|
-
filter = options[:filter].dup
|
77
|
-
filter[:event_type_ids] = event_type_queries.find_event_types(event_types).uniq
|
78
|
-
filter.delete(:event_types)
|
79
|
-
options.merge(filter: filter)
|
80
|
-
end
|
81
|
-
|
82
|
-
# @param array [Array]
|
83
|
-
# @return [String] positional variables, based on array size. Example: "$1, $2, $3"
|
84
|
-
def positional_vars(array)
|
85
|
-
array.size.times.map { |t| "$#{t + 1}" }.join(', ')
|
64
|
+
QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options)
|
86
65
|
end
|
87
66
|
|
88
67
|
# @return [PgEventstore::EventTypeQueries]
|
89
68
|
def event_type_queries
|
90
69
|
EventTypeQueries.new(connection)
|
91
70
|
end
|
71
|
+
|
72
|
+
# @return [PgEventstore::Preloader]
|
73
|
+
def preloader
|
74
|
+
Preloader.new(connection)
|
75
|
+
end
|
92
76
|
end
|
93
77
|
end
|
@@ -33,6 +33,17 @@ module PgEventstore
|
|
33
33
|
end.to_a.dig(0, 'id')
|
34
34
|
end
|
35
35
|
|
36
|
+
# @param ids [Array<Integer>]
|
37
|
+
# @return [Array<Hash>]
|
38
|
+
def find_by_ids(ids)
|
39
|
+
return [] if ids.empty?
|
40
|
+
|
41
|
+
builder = SQLBuilder.new.from('event_types').where('id = ANY(?)', ids.uniq)
|
42
|
+
connection.with do |conn|
|
43
|
+
conn.exec_params(*builder.to_exec_params)
|
44
|
+
end.to_a
|
45
|
+
end
|
46
|
+
|
36
47
|
# @param types [Array<String>]
|
37
48
|
# @return [Array<Integer, nil>]
|
38
49
|
def find_event_types(types)
|
@@ -46,5 +57,18 @@ module PgEventstore
|
|
46
57
|
SQL
|
47
58
|
end.to_a.map { |attrs| attrs['id'] }
|
48
59
|
end
|
60
|
+
|
61
|
+
# Replaces filter by event type strings with filter by event type ids
|
62
|
+
# @param options [Hash]
|
63
|
+
# @return [Hash]
|
64
|
+
def include_event_types_ids(options)
|
65
|
+
options in { filter: { event_types: Array => event_types } }
|
66
|
+
return options unless event_types
|
67
|
+
|
68
|
+
options = Utils.deep_dup(options)
|
69
|
+
options[:filter][:event_type_ids] = find_event_types(event_types).uniq
|
70
|
+
options[:filter].delete(:event_types)
|
71
|
+
options
|
72
|
+
end
|
49
73
|
end
|
50
74
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class Preloader
|
6
|
+
attr_reader :connection
|
7
|
+
private :connection
|
8
|
+
|
9
|
+
# @param connection [PgEventstore::Connection]
|
10
|
+
def initialize(connection)
|
11
|
+
@connection = connection
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param raw_events [Array<Hash>]
|
15
|
+
# @return [Array<Hash>]
|
16
|
+
def preload_related_objects(raw_events)
|
17
|
+
streams = stream_queries.find_by_ids(raw_events.map { _1['stream_id'] }).to_h { [_1['id'], _1] }
|
18
|
+
types = event_type_queries.find_by_ids(raw_events.map { _1['event_type_id'] }).to_h { [_1['id'], _1] }
|
19
|
+
raw_events.each do |event|
|
20
|
+
event['stream'] = streams[event['stream_id']]
|
21
|
+
event['type'] = types[event['event_type_id']]['type']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @return [PgEventstore::EventTypeQueries]
|
28
|
+
def event_type_queries
|
29
|
+
EventTypeQueries.new(connection)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [PgEventstore::StreamQueries]
|
33
|
+
def stream_queries
|
34
|
+
StreamQueries.new(connection)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -26,6 +26,17 @@ module PgEventstore
|
|
26
26
|
deserialize(pg_result) if pg_result.ntuples == 1
|
27
27
|
end
|
28
28
|
|
29
|
+
# @param ids [Array<Integer>]
|
30
|
+
# @return [Array<Hash>]
|
31
|
+
def find_by_ids(ids)
|
32
|
+
return [] if ids.empty?
|
33
|
+
|
34
|
+
builder = SQLBuilder.new.from('streams').where('id = ANY(?)', ids.uniq.sort)
|
35
|
+
connection.with do |conn|
|
36
|
+
conn.exec_params(*builder.to_exec_params)
|
37
|
+
end.to_a
|
38
|
+
end
|
39
|
+
|
29
40
|
# @param stream [PgEventstore::Stream]
|
30
41
|
# @return [PgEventstore::RawStream] persisted stream
|
31
42
|
def create_stream(stream)
|
@@ -44,13 +55,15 @@ module PgEventstore
|
|
44
55
|
end
|
45
56
|
|
46
57
|
# @param stream [PgEventstore::Stream] persisted stream
|
47
|
-
# @return [
|
58
|
+
# @return [PgEventstore::Stream]
|
48
59
|
def update_stream_revision(stream, revision)
|
49
60
|
connection.with do |conn|
|
50
61
|
conn.exec_params(<<~SQL, [revision, stream.id])
|
51
62
|
UPDATE streams SET stream_revision = $1 WHERE id = $2
|
52
63
|
SQL
|
53
64
|
end
|
65
|
+
stream.stream_revision = revision
|
66
|
+
stream
|
54
67
|
end
|
55
68
|
|
56
69
|
private
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class SubscriptionCommandQueries
|
6
|
+
attr_reader :connection
|
7
|
+
private :connection
|
8
|
+
|
9
|
+
# @param connection [PgEventstore::Connection]
|
10
|
+
def initialize(connection)
|
11
|
+
@connection = connection
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param subscription_id [Integer]
|
15
|
+
# @param command_name [String]
|
16
|
+
# @return [Hash, nil]
|
17
|
+
def find_by(subscription_id:, command_name:)
|
18
|
+
sql_builder =
|
19
|
+
SQLBuilder.new.
|
20
|
+
select('*').
|
21
|
+
from('subscription_commands').
|
22
|
+
where('subscription_id = ? AND name = ?', subscription_id, command_name)
|
23
|
+
pg_result = connection.with do |conn|
|
24
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
25
|
+
end
|
26
|
+
return if pg_result.ntuples.zero?
|
27
|
+
|
28
|
+
deserialize(pg_result.to_a.first)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param subscription_id [Integer]
|
32
|
+
# @param command_name [String]
|
33
|
+
# @return [Hash]
|
34
|
+
def create_by(subscription_id:, command_name:)
|
35
|
+
sql = <<~SQL
|
36
|
+
INSERT INTO subscription_commands (name, subscription_id)
|
37
|
+
VALUES ($1, $2)
|
38
|
+
RETURNING *
|
39
|
+
SQL
|
40
|
+
pg_result = connection.with do |conn|
|
41
|
+
conn.exec_params(sql, [command_name, subscription_id])
|
42
|
+
end
|
43
|
+
deserialize(pg_result.to_a.first)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param subscription_ids [Array<Integer>]
|
47
|
+
# @return [Array<Hash>]
|
48
|
+
def find_commands(subscription_ids)
|
49
|
+
return [] if subscription_ids.empty?
|
50
|
+
|
51
|
+
sql = subscription_ids.size.times.map do
|
52
|
+
"?"
|
53
|
+
end.join(", ")
|
54
|
+
sql_builder =
|
55
|
+
SQLBuilder.new.select('*').
|
56
|
+
from('subscription_commands').
|
57
|
+
where("subscription_id IN (#{sql})", *subscription_ids).
|
58
|
+
order('id ASC')
|
59
|
+
pg_result = connection.with do |conn|
|
60
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
61
|
+
end
|
62
|
+
pg_result.to_a.map(&method(:deserialize))
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param id [Integer]
|
66
|
+
# @return [void]
|
67
|
+
def delete(id)
|
68
|
+
connection.with do |conn|
|
69
|
+
conn.exec_params('DELETE FROM subscription_commands WHERE id = $1', [id])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# @param hash [Hash]
|
76
|
+
# @return [Hash]
|
77
|
+
def deserialize(hash)
|
78
|
+
hash.transform_keys(&:to_sym)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|