pg_eventstore 0.3.0 → 0.4.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 +4 -0
- data/README.md +1 -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/9_create_subscriptions.sql +46 -0
- data/docs/configuration.md +42 -21
- data/docs/subscriptions.md +170 -0
- data/lib/pg_eventstore/callbacks.rb +122 -0
- data/lib/pg_eventstore/client.rb +2 -2
- data/lib/pg_eventstore/config.rb +35 -3
- data/lib/pg_eventstore/errors.rb +63 -0
- data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
- data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
- data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
- data/lib/pg_eventstore/queries/event_queries.rb +5 -26
- data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +160 -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 +6 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
- data/lib/pg_eventstore/sql_builder.rb +54 -10
- 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 +1 -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 +30 -4
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module PgEventstore
|
4
4
|
# @!visibility private
|
5
|
-
class
|
5
|
+
class EventDeserializer
|
6
6
|
attr_reader :middlewares, :event_class_resolver
|
7
7
|
|
8
8
|
# @param middlewares [Array<Object<#deserialize, #serialize>>]
|
@@ -14,29 +14,21 @@ module PgEventstore
|
|
14
14
|
|
15
15
|
# @param pg_result [PG::Result]
|
16
16
|
# @return [Array<PgEventstore::Event>]
|
17
|
-
def
|
18
|
-
pg_result.map(&method(:
|
17
|
+
def deserialize_pg_result(pg_result)
|
18
|
+
pg_result.map(&method(:deserialize))
|
19
19
|
end
|
20
|
-
alias deserialize_many deserialize
|
21
20
|
|
22
21
|
# @param pg_result [PG::Result]
|
23
22
|
# @return [PgEventstore::Event, nil]
|
24
|
-
def
|
23
|
+
def deserialize_one_pg_result(pg_result)
|
25
24
|
return if pg_result.ntuples.zero?
|
26
25
|
|
27
|
-
|
26
|
+
deserialize(pg_result.first)
|
28
27
|
end
|
29
28
|
|
30
|
-
# @return [PgEventstore::PgResultDeserializer]
|
31
|
-
def without_middlewares
|
32
|
-
self.class.new([], event_class_resolver)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
29
|
# @param attrs [Hash]
|
38
30
|
# @return [PgEventstore::Event]
|
39
|
-
def
|
31
|
+
def deserialize(attrs)
|
40
32
|
event = event_class_resolver.call(attrs['type']).new(**attrs.transform_keys(&:to_sym))
|
41
33
|
middlewares.each do |middleware|
|
42
34
|
middleware.deserialize(event)
|
@@ -44,5 +36,10 @@ module PgEventstore
|
|
44
36
|
event.stream = PgEventstore::Stream.new(**attrs['stream'].transform_keys(&:to_sym)) if attrs.key?('stream')
|
45
37
|
event
|
46
38
|
end
|
39
|
+
|
40
|
+
# @return [PgEventstore::EventDeserializer]
|
41
|
+
def without_middlewares
|
42
|
+
self.class.new([], event_class_resolver)
|
43
|
+
end
|
47
44
|
end
|
48
45
|
end
|
@@ -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
|
@@ -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
|
@@ -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,12 @@ 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
|
pg_result = connection.with do |conn|
|
28
26
|
conn.exec_params(*exec_params)
|
29
27
|
end
|
30
|
-
deserializer.
|
28
|
+
deserializer.deserialize_pg_result(pg_result)
|
31
29
|
end
|
32
30
|
|
33
31
|
# @param stream [PgEventstore::Stream] persisted stream
|
@@ -42,14 +40,14 @@ module PgEventstore
|
|
42
40
|
|
43
41
|
sql = <<~SQL
|
44
42
|
INSERT INTO events (#{attributes.keys.join(', ')})
|
45
|
-
VALUES (#{positional_vars(attributes.values)})
|
43
|
+
VALUES (#{Utils.positional_vars(attributes.values)})
|
46
44
|
RETURNING *, $#{attributes.values.size + 1} as type
|
47
45
|
SQL
|
48
46
|
|
49
47
|
pg_result = connection.with do |conn|
|
50
48
|
conn.exec_params(sql, [*attributes.values, event.type])
|
51
49
|
end
|
52
|
-
deserializer.without_middlewares.
|
50
|
+
deserializer.without_middlewares.deserialize_one_pg_result(pg_result).tap do |persisted_event|
|
53
51
|
persisted_event.stream = stream
|
54
52
|
end
|
55
53
|
end
|
@@ -66,25 +64,6 @@ module PgEventstore
|
|
66
64
|
QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options, offset: offset)
|
67
65
|
end
|
68
66
|
|
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(', ')
|
86
|
-
end
|
87
|
-
|
88
67
|
# @return [PgEventstore::EventTypeQueries]
|
89
68
|
def event_type_queries
|
90
69
|
EventTypeQueries.new(connection)
|
@@ -46,5 +46,18 @@ module PgEventstore
|
|
46
46
|
SQL
|
47
47
|
end.to_a.map { |attrs| attrs['id'] }
|
48
48
|
end
|
49
|
+
|
50
|
+
# Replaces filter by event type strings with filter by event type ids
|
51
|
+
# @param options [Hash]
|
52
|
+
# @return [Hash]
|
53
|
+
def include_event_types_ids(options)
|
54
|
+
options in { filter: { event_types: Array => event_types } }
|
55
|
+
return options unless event_types
|
56
|
+
|
57
|
+
options = Utils.deep_dup(options)
|
58
|
+
options[:filter][:event_type_ids] = find_event_types(event_types).uniq
|
59
|
+
options[:filter].delete(:event_types)
|
60
|
+
options
|
61
|
+
end
|
49
62
|
end
|
50
63
|
end
|
@@ -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
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class SubscriptionQueries
|
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 attrs [Hash]
|
15
|
+
# @return [Hash]
|
16
|
+
def find_or_create_by(attrs)
|
17
|
+
transaction_queries.transaction do
|
18
|
+
find_by(attrs) || create(attrs)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param attrs [Hash]
|
23
|
+
# @return [Hash, nil]
|
24
|
+
def find_by(attrs)
|
25
|
+
builder = SQLBuilder.new.select('*').from('subscriptions')
|
26
|
+
attrs.each do |attr, val|
|
27
|
+
builder.where("#{attr} = ?", val)
|
28
|
+
end
|
29
|
+
|
30
|
+
pg_result = connection.with do |conn|
|
31
|
+
conn.exec_params(*builder.to_exec_params)
|
32
|
+
end
|
33
|
+
return if pg_result.ntuples.zero?
|
34
|
+
|
35
|
+
deserialize(pg_result.to_a.first)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param id [Integer]
|
39
|
+
# @return [Hash]
|
40
|
+
# @raise [PgEventstore::RecordNotFound]
|
41
|
+
def find!(id)
|
42
|
+
find_by(id: id) || raise(RecordNotFound.new("subscriptions", id))
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param attrs [Hash]
|
46
|
+
# @return [Hash]
|
47
|
+
def create(attrs)
|
48
|
+
sql = <<~SQL
|
49
|
+
INSERT INTO subscriptions (#{attrs.keys.join(', ')})
|
50
|
+
VALUES (#{Utils.positional_vars(attrs.values)})
|
51
|
+
RETURNING *
|
52
|
+
SQL
|
53
|
+
pg_result = connection.with do |conn|
|
54
|
+
conn.exec_params(sql, attrs.values)
|
55
|
+
end
|
56
|
+
deserialize(pg_result.to_a.first)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param id [Integer]
|
60
|
+
# @param attrs [Hash]
|
61
|
+
def update(id, attrs)
|
62
|
+
attrs = { updated_at: Time.now.utc }.merge(attrs)
|
63
|
+
attrs_sql = attrs.keys.map.with_index(1) do |attr, index|
|
64
|
+
"#{attr} = $#{index}"
|
65
|
+
end.join(', ')
|
66
|
+
sql =
|
67
|
+
"UPDATE subscriptions SET #{attrs_sql} WHERE id = $#{attrs.keys.size + 1} RETURNING *"
|
68
|
+
pg_result = connection.with do |conn|
|
69
|
+
conn.exec_params(sql, [*attrs.values, id])
|
70
|
+
end
|
71
|
+
raise(RecordNotFound.new("subscriptions", id)) if pg_result.ntuples.zero?
|
72
|
+
|
73
|
+
deserialize(pg_result.to_a.first)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param query_options [Array<Array<Integer, Hash>>] array of runner ids and query options
|
77
|
+
# @return [Array<Hash>] array of raw events
|
78
|
+
def subscriptions_events(query_options)
|
79
|
+
return [] if query_options.empty?
|
80
|
+
|
81
|
+
final_builder = union_builders(query_options.map { |id, opts| query_builder(id, opts) })
|
82
|
+
connection.with do |conn|
|
83
|
+
conn.exec_params(*final_builder.to_exec_params)
|
84
|
+
end.to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param id [Integer] subscription's id
|
88
|
+
# @param lock_id [String] UUIDv4 id of the subscriptions set which reserves the subscription
|
89
|
+
# @param force [Boolean] whether to lock the subscription despite on #locked_by value
|
90
|
+
# @return [String] UUIDv4 lock id
|
91
|
+
# @raise [SubscriptionAlreadyLockedError] in case the Subscription is already locked
|
92
|
+
def lock!(id, lock_id, force = false)
|
93
|
+
transaction_queries.transaction do
|
94
|
+
attrs = find!(id)
|
95
|
+
if attrs[:locked_by] && !force
|
96
|
+
raise SubscriptionAlreadyLockedError.new(attrs[:set], attrs[:name], attrs[:locked_by])
|
97
|
+
end
|
98
|
+
connection.with do |conn|
|
99
|
+
conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [lock_id, id])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
lock_id
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param id [Integer] subscription's id
|
106
|
+
# @param lock_id [String] UUIDv4 id of the set which reserved the subscription after itself
|
107
|
+
# @return [void]
|
108
|
+
# @raise [SubscriptionUnlockError] in case the Subscription is locked by some SubscriptionsSet, other than the one,
|
109
|
+
# persisted in memory
|
110
|
+
def unlock!(id, lock_id)
|
111
|
+
transaction_queries.transaction do
|
112
|
+
attrs = find!(id)
|
113
|
+
# Normally this should never happen as locking/unlocking happens within the same process. This is done only for
|
114
|
+
# the matter of consistency.
|
115
|
+
unless attrs[:locked_by] == lock_id
|
116
|
+
raise SubscriptionUnlockError.new(attrs[:set], attrs[:name], lock_id, attrs[:locked_by])
|
117
|
+
end
|
118
|
+
connection.with do |conn|
|
119
|
+
conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [nil, id])
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# @param id [Integer] runner id
|
127
|
+
# @param options [Hash] query options
|
128
|
+
# @return [PgEventstore::SQLBuilder]
|
129
|
+
def query_builder(id, options)
|
130
|
+
builder = PgEventstore::QueryBuilders::EventsFiltering.subscriptions_events_filtering(
|
131
|
+
event_type_queries.include_event_types_ids(options)
|
132
|
+
).to_sql_builder
|
133
|
+
builder.select("#{id} as runner_id")
|
134
|
+
end
|
135
|
+
|
136
|
+
# @param builders [Array<PgEventstore::SQLBuilder>]
|
137
|
+
# @return [PgEventstore::SQLBuilder]
|
138
|
+
def union_builders(builders)
|
139
|
+
builders[1..].each_with_object(builders[0]) do |builder, first_builder|
|
140
|
+
first_builder.union(builder)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# @return [PgEventstore::TransactionQueries]
|
145
|
+
def transaction_queries
|
146
|
+
TransactionQueries.new(connection)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [PgEventstore::EventTypeQueries]
|
150
|
+
def event_type_queries
|
151
|
+
EventTypeQueries.new(connection)
|
152
|
+
end
|
153
|
+
|
154
|
+
# @param hash [Hash]
|
155
|
+
# @return [Hash]
|
156
|
+
def deserialize(hash)
|
157
|
+
hash.transform_keys(&:to_sym)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class SubscriptionsSetCommandQueries
|
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 subscriptions_set_id [Integer]
|
15
|
+
# @param command_name [String]
|
16
|
+
# @return [Hash, nil]
|
17
|
+
def find_by(subscriptions_set_id:, command_name:)
|
18
|
+
sql_builder =
|
19
|
+
SQLBuilder.new.
|
20
|
+
select('*').
|
21
|
+
from('subscriptions_set_commands').
|
22
|
+
where('subscriptions_set_id = ? AND name = ?', subscriptions_set_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 subscriptions_set_id [Integer]
|
32
|
+
# @param command_name [String]
|
33
|
+
# @return [Hash]
|
34
|
+
def create_by(subscriptions_set_id:, command_name:)
|
35
|
+
sql = <<~SQL
|
36
|
+
INSERT INTO subscriptions_set_commands (name, subscriptions_set_id)
|
37
|
+
VALUES ($1, $2)
|
38
|
+
RETURNING *
|
39
|
+
SQL
|
40
|
+
pg_result = connection.with do |conn|
|
41
|
+
conn.exec_params(sql, [command_name, subscriptions_set_id])
|
42
|
+
end
|
43
|
+
deserialize(pg_result.to_a.first)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param subscriptions_set_id [Integer]
|
47
|
+
# @return [Array<Hash>]
|
48
|
+
def find_commands(subscriptions_set_id)
|
49
|
+
sql_builder =
|
50
|
+
SQLBuilder.new.select('*').
|
51
|
+
from('subscriptions_set_commands').
|
52
|
+
where("subscriptions_set_id = ?", subscriptions_set_id).
|
53
|
+
order('id ASC')
|
54
|
+
pg_result = connection.with do |conn|
|
55
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
56
|
+
end
|
57
|
+
pg_result.to_a.map(&method(:deserialize))
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param id [Integer]
|
61
|
+
# @return [void]
|
62
|
+
def delete(id)
|
63
|
+
connection.with do |conn|
|
64
|
+
conn.exec_params('DELETE FROM subscriptions_set_commands WHERE id = $1', [id])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# @param hash [Hash]
|
71
|
+
# @return [Hash]
|
72
|
+
def deserialize(hash)
|
73
|
+
hash.transform_keys(&:to_sym)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|