pg_eventstore 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/db/extensions.sql +2 -0
- data/db/indexes.sql +13 -0
- data/db/primary_and_foreign_keys.sql +11 -0
- data/db/tables.sql +21 -0
- data/docs/appending_events.md +170 -0
- data/docs/configuration.md +82 -0
- data/docs/events_and_streams.md +45 -0
- data/docs/multiple_commands.md +46 -0
- data/docs/reading_events.md +161 -0
- data/docs/writing_middleware.md +160 -0
- data/lib/pg_eventstore/abstract_command.rb +18 -0
- data/lib/pg_eventstore/client.rb +133 -0
- data/lib/pg_eventstore/commands/append.rb +61 -0
- data/lib/pg_eventstore/commands/multiple.rb +14 -0
- data/lib/pg_eventstore/commands/read.rb +24 -0
- data/lib/pg_eventstore/commands.rb +6 -0
- data/lib/pg_eventstore/config.rb +30 -0
- data/lib/pg_eventstore/connection.rb +97 -0
- data/lib/pg_eventstore/errors.rb +107 -0
- data/lib/pg_eventstore/event.rb +59 -0
- data/lib/pg_eventstore/event_class_resolver.rb +17 -0
- data/lib/pg_eventstore/event_serializer.rb +27 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +103 -0
- data/lib/pg_eventstore/middleware.rb +15 -0
- data/lib/pg_eventstore/pg_result_deserializer.rb +48 -0
- data/lib/pg_eventstore/queries.rb +127 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +187 -0
- data/lib/pg_eventstore/rspec/has_option_matcher.rb +90 -0
- data/lib/pg_eventstore/sql_builder.rb +126 -0
- data/lib/pg_eventstore/stream.rb +72 -0
- data/lib/pg_eventstore/tasks/setup.rake +37 -0
- data/lib/pg_eventstore/version.rb +5 -0
- data/lib/pg_eventstore.rb +85 -0
- data/pg_eventstore.gemspec +40 -0
- metadata +113 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module QueryBuilders
|
5
|
+
# @!visibility private
|
6
|
+
class EventsFiltering
|
7
|
+
DEFAULT_OFFSET = 0
|
8
|
+
DEFAULT_LIMIT = 1000
|
9
|
+
SQL_DIRECTIONS = {
|
10
|
+
'asc' => 'ASC',
|
11
|
+
'desc' => 'DESC',
|
12
|
+
:asc => 'ASC',
|
13
|
+
:desc => 'DESC',
|
14
|
+
'Forwards' => 'ASC',
|
15
|
+
'Backwards' => 'DESC'
|
16
|
+
}.tap do |directions|
|
17
|
+
directions.default = 'ASC'
|
18
|
+
end.freeze
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# @param options [Hash]
|
22
|
+
# @param offset [Integer]
|
23
|
+
# @return [PgEventstore::QueryBuilders::EventsFiltering]
|
24
|
+
def all_stream_filtering(options, offset: 0)
|
25
|
+
event_filter = new
|
26
|
+
options in { filter: { event_types: Array => event_types } }
|
27
|
+
event_filter.add_event_types(event_types)
|
28
|
+
event_filter.add_limit(options[:max_count])
|
29
|
+
event_filter.add_offset(offset)
|
30
|
+
event_filter.resolve_links(options[:resolve_link_tos])
|
31
|
+
options in { filter: { streams: Array => streams } }
|
32
|
+
streams&.each { |attrs| event_filter.add_stream_attrs(**attrs) }
|
33
|
+
event_filter.add_global_position(options[:from_position], options[:direction])
|
34
|
+
event_filter.add_all_stream_direction(options[:direction])
|
35
|
+
event_filter
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param stream [PgEventstore::Stream]
|
39
|
+
# @param options [Hash]
|
40
|
+
# @param offset [Integer]
|
41
|
+
# @return [PgEventstore::QueryBuilders::EventsFiltering]
|
42
|
+
def specific_stream_filtering(stream, options, offset: 0)
|
43
|
+
event_filter = new
|
44
|
+
options in { filter: { event_types: Array => event_types } }
|
45
|
+
event_filter.add_event_types(event_types)
|
46
|
+
event_filter.add_limit(options[:max_count])
|
47
|
+
event_filter.add_offset(offset)
|
48
|
+
event_filter.resolve_links(options[:resolve_link_tos])
|
49
|
+
event_filter.add_stream(stream)
|
50
|
+
event_filter.add_revision(options[:from_revision], options[:direction])
|
51
|
+
event_filter.add_stream_direction(options[:direction])
|
52
|
+
event_filter
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize
|
57
|
+
@sql_builder =
|
58
|
+
SQLBuilder.new.
|
59
|
+
select('events.*').
|
60
|
+
select('row_to_json(streams.*) as stream').
|
61
|
+
from('events').
|
62
|
+
join('JOIN streams ON streams.id = events.stream_id').
|
63
|
+
limit(DEFAULT_LIMIT).
|
64
|
+
offset(DEFAULT_OFFSET)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param context [String, nil]
|
68
|
+
# @param stream_name [String, nil]
|
69
|
+
# @param stream_id [String, nil]
|
70
|
+
# @return [void]
|
71
|
+
def add_stream_attrs(context: nil, stream_name: nil, stream_id: nil)
|
72
|
+
stream_attrs = { context: context, stream_name: stream_name, stream_id: stream_id }
|
73
|
+
return unless correct_stream_filter?(stream_attrs)
|
74
|
+
|
75
|
+
stream_attrs.compact!
|
76
|
+
sql = stream_attrs.map do |attr, _|
|
77
|
+
"streams.#{attr} = ?"
|
78
|
+
end.join(" AND ")
|
79
|
+
@sql_builder.where_or(sql, *stream_attrs.values)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param stream [PgEventstore::Stream]
|
83
|
+
# @return [void]
|
84
|
+
def add_stream(stream)
|
85
|
+
@sql_builder.where("streams.id = ?", stream.id)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @param event_types [Array, nil]
|
89
|
+
# @return [void]
|
90
|
+
def add_event_types(event_types)
|
91
|
+
return if event_types.nil?
|
92
|
+
return if event_types.empty?
|
93
|
+
|
94
|
+
sql = event_types.size.times.map do
|
95
|
+
"events.type = ?"
|
96
|
+
end.join(" OR ")
|
97
|
+
@sql_builder.where(sql, *event_types)
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param revision [Integer, nil]
|
101
|
+
# @param direction [String, Symbol, nil]
|
102
|
+
# @return [void]
|
103
|
+
def add_revision(revision, direction)
|
104
|
+
return unless revision
|
105
|
+
|
106
|
+
@sql_builder.where("events.stream_revision #{direction_operator(direction)} ?", revision)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param position [Integer, nil]
|
110
|
+
# @param direction [String, Symbol, nil]
|
111
|
+
# @return [void]
|
112
|
+
def add_global_position(position, direction)
|
113
|
+
return unless position
|
114
|
+
|
115
|
+
@sql_builder.where("events.global_position #{direction_operator(direction)} ?", position)
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param direction [String, Symbol, nil]
|
119
|
+
# @return [void]
|
120
|
+
def add_stream_direction(direction)
|
121
|
+
@sql_builder.order("events.stream_revision #{SQL_DIRECTIONS[direction]}")
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param direction [String, Symbol, nil]
|
125
|
+
# @return [void]
|
126
|
+
def add_all_stream_direction(direction)
|
127
|
+
@sql_builder.order("events.global_position #{SQL_DIRECTIONS[direction]}")
|
128
|
+
end
|
129
|
+
|
130
|
+
# @param limit [Integer, nil]
|
131
|
+
# @return [void]
|
132
|
+
def add_limit(limit)
|
133
|
+
return unless limit
|
134
|
+
|
135
|
+
@sql_builder.limit(limit)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @param offset [Integer, nil]
|
139
|
+
# @return [void]
|
140
|
+
def add_offset(offset)
|
141
|
+
return unless offset
|
142
|
+
|
143
|
+
@sql_builder.offset(offset)
|
144
|
+
end
|
145
|
+
|
146
|
+
# @param should_resolve [Boolean]
|
147
|
+
# @return [void]
|
148
|
+
def resolve_links(should_resolve)
|
149
|
+
return unless should_resolve
|
150
|
+
|
151
|
+
@sql_builder.
|
152
|
+
unselect.
|
153
|
+
select("(COALESCE(original_events.*, events.*)).*").
|
154
|
+
select('row_to_json(streams.*) as stream').
|
155
|
+
join("LEFT JOIN events original_events ON original_events.id = events.link_id")
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Array]
|
159
|
+
def to_exec_params
|
160
|
+
@sql_builder.to_exec_params
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
# @param stream_attrs [Hash]
|
166
|
+
# @return [Boolean]
|
167
|
+
def correct_stream_filter?(stream_attrs)
|
168
|
+
result = (stream_attrs in { context: String, stream_name: String, stream_id: String } |
|
169
|
+
{ context: String, stream_name: String, stream_id: nil } |
|
170
|
+
{ context: String, stream_name: nil, stream_id: nil })
|
171
|
+
return true if result
|
172
|
+
|
173
|
+
PgEventstore&.logger&.debug(<<~TEXT)
|
174
|
+
Ignoring unsupported stream filter format for searching #{stream_attrs.compact.inspect}. \
|
175
|
+
See docs/reading_events.md docs for supported formats.
|
176
|
+
TEXT
|
177
|
+
false
|
178
|
+
end
|
179
|
+
|
180
|
+
# @param direction [String, Symbol, nil]
|
181
|
+
# @return [String]
|
182
|
+
def direction_operator(direction)
|
183
|
+
SQL_DIRECTIONS[direction] == 'ASC' ? '>=' : '<='
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This matcher is defined to test options which are defined by using
|
4
|
+
# EventStoreClient::Extensions::OptionsExtension option. Example:
|
5
|
+
# Let's say you have next class
|
6
|
+
# class SomeClass
|
7
|
+
# include PgEventstore::Extensions::OptionsExtension
|
8
|
+
#
|
9
|
+
# option(:some_opt) { '1' }
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# To test that its instance has the proper option with the proper default value you can use this
|
13
|
+
# matcher:
|
14
|
+
# RSpec.describe SomeClass do
|
15
|
+
# subject { described_class.new }
|
16
|
+
#
|
17
|
+
# # Check that :some_opt is present
|
18
|
+
# it { is_expected.to have_option(:some_opt) }
|
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
|
45
|
+
# end
|
46
|
+
RSpec::Matchers.define :has_option do |option_name|
|
47
|
+
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
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
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}"
|
62
|
+
else
|
63
|
+
msg = "Expected #{obj} to have `#{option_name.inspect}' option."
|
64
|
+
end
|
65
|
+
|
66
|
+
msg
|
67
|
+
end
|
68
|
+
|
69
|
+
description do
|
70
|
+
expected_list = RSpec::Matchers::EnglishPhrasing.list(expected)
|
71
|
+
sentences =
|
72
|
+
@chained_method_clauses.map do |(method_name, method_args)|
|
73
|
+
next '' if method_name == :required_kwargs
|
74
|
+
|
75
|
+
english_name = RSpec::Matchers::EnglishPhrasing.split_words(method_name)
|
76
|
+
arg_list = RSpec::Matchers::EnglishPhrasing.list(method_args)
|
77
|
+
" #{english_name}#{arg_list}"
|
78
|
+
end.join
|
79
|
+
|
80
|
+
"have#{expected_list} option#{sentences}"
|
81
|
+
end
|
82
|
+
|
83
|
+
chain :with_default_value do |val|
|
84
|
+
@default_value = val
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
RSpec::Matchers.alias_matcher :have_option, :has_option
|
89
|
+
RSpec::Matchers.alias_matcher :has_attribute, :has_option
|
90
|
+
RSpec::Matchers.alias_matcher :have_attribute, :has_option
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# Deadly simple SQL builder
|
5
|
+
# @!visibility private
|
6
|
+
class SQLBuilder
|
7
|
+
def initialize
|
8
|
+
@select_values = []
|
9
|
+
@from_value = nil
|
10
|
+
@where_values = { 'AND' => [], 'OR' => [] }
|
11
|
+
@join_values = []
|
12
|
+
@order_values = []
|
13
|
+
@limit_value = nil
|
14
|
+
@offset_value = nil
|
15
|
+
@positional_values = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param sql [String]
|
19
|
+
# @return self
|
20
|
+
def select(sql)
|
21
|
+
@select_values.push(sql)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return self
|
26
|
+
def unselect
|
27
|
+
@select_values.clear
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param sql [String]
|
32
|
+
# @param arguments [Array] positional values
|
33
|
+
# @return self
|
34
|
+
def where(sql, *arguments)
|
35
|
+
sql = extract_positional_args(sql, *arguments)
|
36
|
+
@where_values['AND'].push("(#{sql})")
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param sql [String]
|
41
|
+
# @param arguments [Object] positional values
|
42
|
+
# @return self
|
43
|
+
def where_or(sql, *arguments)
|
44
|
+
sql = extract_positional_args(sql, *arguments)
|
45
|
+
@where_values['OR'].push("(#{sql})")
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param table_name [String]
|
50
|
+
# @return self
|
51
|
+
def from(table_name)
|
52
|
+
@from_value = table_name
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param sql [String]
|
57
|
+
# @param arguments [Object]
|
58
|
+
# @return self
|
59
|
+
def join(sql, *arguments)
|
60
|
+
@join_values.push(extract_positional_args(sql, *arguments))
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param sql [String]
|
65
|
+
# @return self
|
66
|
+
def order(sql)
|
67
|
+
@order_values.push(sql)
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param limit [Integer]
|
72
|
+
# @return self
|
73
|
+
def limit(limit)
|
74
|
+
@limit_value = limit.to_i
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param offset [Integer]
|
79
|
+
# @return self
|
80
|
+
def offset(offset)
|
81
|
+
@offset_value = offset.to_i
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_exec_params
|
86
|
+
where_sql = [where_sql('OR'), where_sql('AND')].reject(&:empty?).map { |sql| "(#{sql})" }.join(' AND ')
|
87
|
+
sql = "SELECT #{select_sql} FROM #{@from_value}"
|
88
|
+
sql += " #{join_sql}" unless @join_values.empty?
|
89
|
+
sql += " WHERE #{where_sql}" unless where_sql.empty?
|
90
|
+
sql += " ORDER BY #{order_sql}" unless @order_values.empty?
|
91
|
+
sql += " LIMIT #{@limit_value}" if @limit_value
|
92
|
+
sql += " OFFSET #{@offset_value}" if @offset_value
|
93
|
+
[sql, @positional_values]
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# @return [String]
|
99
|
+
def select_sql
|
100
|
+
@select_values.empty? ? '*' : @select_values.join(', ')
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param join_pattern [String] "OR"/"AND"
|
104
|
+
# @return [String]
|
105
|
+
def where_sql(join_pattern)
|
106
|
+
@where_values[join_pattern].join(" #{join_pattern} ")
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [String]
|
110
|
+
def join_sql
|
111
|
+
@join_values.join(" ")
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [String]
|
115
|
+
def order_sql
|
116
|
+
@order_values.join(', ')
|
117
|
+
end
|
118
|
+
|
119
|
+
def extract_positional_args(sql, *arguments)
|
120
|
+
sql.gsub("?").each_with_index do |_, index|
|
121
|
+
@positional_values.push(arguments[index])
|
122
|
+
"$#{@positional_values.size}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
module PgEventstore
|
6
|
+
class Stream
|
7
|
+
SYSTEM_STREAM_PREFIX = '$'
|
8
|
+
INITIAL_STREAM_REVISION = -1 # this is the default value of streams.stream_revision column
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Produces "all" stream instance. "all" stream does not represent any specific stream. Instead, it indicates that
|
12
|
+
# a specific command should be performed over any kind of streams if possible
|
13
|
+
# @return [PgEventstore::Stream]
|
14
|
+
def all_stream
|
15
|
+
allocate.tap do |stream|
|
16
|
+
stream.instance_variable_set(:@all_stream, true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :context, :stream_name, :stream_id, :id, :stream_revision
|
22
|
+
|
23
|
+
# @param context [String]
|
24
|
+
# @param stream_name [String]
|
25
|
+
# @param stream_id [String]
|
26
|
+
# @param id [Integer, nil] internal stream's id, read only
|
27
|
+
# @param stream_revision [Integer, nil] current stream revision, read only
|
28
|
+
def initialize(context:, stream_name:, stream_id:, id: nil, stream_revision: nil)
|
29
|
+
@context = context
|
30
|
+
@stream_name = stream_name
|
31
|
+
@stream_id = stream_id
|
32
|
+
@id = id
|
33
|
+
@stream_revision = stream_revision
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean]
|
37
|
+
def all_stream?
|
38
|
+
!!@all_stream
|
39
|
+
end
|
40
|
+
|
41
|
+
# Determine whether a stream is reserved by `pg_eventstore`. You can't append events to such streams.
|
42
|
+
# @return [Boolean]
|
43
|
+
def system?
|
44
|
+
all_stream? || context.start_with?(SYSTEM_STREAM_PREFIX)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Array]
|
48
|
+
def deconstruct
|
49
|
+
[context, stream_name, stream_id]
|
50
|
+
end
|
51
|
+
alias to_a deconstruct
|
52
|
+
|
53
|
+
# @param keys [Array<Symbol>, nil]
|
54
|
+
def deconstruct_keys(keys)
|
55
|
+
hash = { context: context, stream_name: stream_name, stream_id: stream_id }
|
56
|
+
return hash unless keys
|
57
|
+
|
58
|
+
hash.slice(*keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Hash]
|
62
|
+
def to_hash
|
63
|
+
deconstruct_keys(nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
def ==(other_stream)
|
67
|
+
return false unless other_stream.is_a?(Stream)
|
68
|
+
|
69
|
+
to_hash == other_stream.to_hash
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :pg_eventstore do
|
4
|
+
desc "Creates events table, indexes, etc."
|
5
|
+
task :create do
|
6
|
+
PgEventstore.configure do |config|
|
7
|
+
config.pg_uri = ENV['PG_EVENTSTORE_URI']
|
8
|
+
end
|
9
|
+
|
10
|
+
db_files_root = "#{Gem::Specification.find_by_name("pg_eventstore").gem_dir}/db"
|
11
|
+
|
12
|
+
PgEventstore.connection.with do |conn|
|
13
|
+
conn.transaction do
|
14
|
+
conn.exec(File.read("#{db_files_root}/extensions.sql"))
|
15
|
+
conn.exec(File.read("#{db_files_root}/tables.sql"))
|
16
|
+
conn.exec(File.read("#{db_files_root}/primary_and_foreign_keys.sql"))
|
17
|
+
conn.exec(File.read("#{db_files_root}/indexes.sql"))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Drops events table and related pg_eventstore objects."
|
23
|
+
task :drop do
|
24
|
+
PgEventstore.configure do |config|
|
25
|
+
config.pg_uri = ENV['PG_EVENTSTORE_URI']
|
26
|
+
end
|
27
|
+
|
28
|
+
PgEventstore.connection.with do |conn|
|
29
|
+
conn.exec <<~SQL
|
30
|
+
DROP TABLE IF EXISTS public.events;
|
31
|
+
DROP TABLE IF EXISTS public.streams;
|
32
|
+
DROP EXTENSION IF EXISTS "uuid-ossp";
|
33
|
+
DROP EXTENSION IF EXISTS pgcrypto;
|
34
|
+
SQL
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'pg_eventstore/version'
|
4
|
+
require_relative 'pg_eventstore/extensions/options_extension'
|
5
|
+
require_relative 'pg_eventstore/event_class_resolver'
|
6
|
+
require_relative 'pg_eventstore/config'
|
7
|
+
require_relative 'pg_eventstore/event'
|
8
|
+
require_relative 'pg_eventstore/sql_builder'
|
9
|
+
require_relative 'pg_eventstore/stream'
|
10
|
+
require_relative 'pg_eventstore/client'
|
11
|
+
require_relative 'pg_eventstore/connection'
|
12
|
+
require_relative 'pg_eventstore/errors'
|
13
|
+
require_relative 'pg_eventstore/middleware'
|
14
|
+
|
15
|
+
module PgEventstore
|
16
|
+
class << self
|
17
|
+
attr_reader :mutex
|
18
|
+
private :mutex
|
19
|
+
|
20
|
+
# Creates a Config if not exists and yields it to the given block.
|
21
|
+
# @param name [Symbol] a name to assign to a config
|
22
|
+
# @return [Object] a result of the given block
|
23
|
+
def configure(name: :default)
|
24
|
+
mutex.synchronize do
|
25
|
+
@config[name] ||= Config.new(name: name)
|
26
|
+
connection_config_was = @config[name].connection_options
|
27
|
+
|
28
|
+
yield(@config[name]).tap do
|
29
|
+
next if connection_config_was == @config[name].connection_options
|
30
|
+
|
31
|
+
# Reset the connection if user decided to reconfigure connection's options
|
32
|
+
@connection.delete(name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param name [Symbol]
|
38
|
+
# @return [PgEventstore::Config]
|
39
|
+
def config(name = :default)
|
40
|
+
return @config[name] if @config[name]
|
41
|
+
|
42
|
+
error_message = <<~TEXT
|
43
|
+
Could not find #{name.inspect} config. You can define it like this:
|
44
|
+
PgEventstore.configure(name: #{name.inspect}) do |config|
|
45
|
+
# your config goes here
|
46
|
+
end
|
47
|
+
TEXT
|
48
|
+
raise error_message
|
49
|
+
end
|
50
|
+
|
51
|
+
# Look ups and returns a Connection, based on the given config. If not exists - it creates one. This operation is a
|
52
|
+
# thread-safe
|
53
|
+
# @param name [Symbol]
|
54
|
+
# @return [PgEventstore::Connection]
|
55
|
+
def connection(name = :default)
|
56
|
+
mutex.synchronize do
|
57
|
+
@connection[name] ||= Connection.new(**config(name).connection_options)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param name [Symbol]
|
62
|
+
# @return [PgEventstore::Client]
|
63
|
+
def client(name = :default)
|
64
|
+
Client.new(config(name))
|
65
|
+
end
|
66
|
+
|
67
|
+
def logger
|
68
|
+
@logger
|
69
|
+
end
|
70
|
+
|
71
|
+
def logger=(logger)
|
72
|
+
@logger = logger
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# @return [void]
|
78
|
+
def init_variables
|
79
|
+
@config = { default: Config.new(name: :default) }
|
80
|
+
@connection = {}
|
81
|
+
@mutex = Thread::Mutex.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
init_variables
|
85
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/pg_eventstore/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "pg_eventstore"
|
7
|
+
spec.version = PgEventstore::VERSION
|
8
|
+
spec.authors = ["Ivan Dzyzenko"]
|
9
|
+
spec.email = ["ivan.dzyzenko@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "EventStore implementation using PostgreSQL"
|
12
|
+
spec.description = "EventStore implementation using PostgreSQL"
|
13
|
+
spec.homepage = "https://github.com/yousty/pg_eventstore"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/yousty/pg_eventstore"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/yousty/pg_eventstore/blob/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
paths_to_exclude = %w[
|
27
|
+
bin/ test/ spec/ features/ .git .circleci appveyor Gemfile .ruby-version .ruby-gemset .rspec docker-compose.yml
|
28
|
+
Rakefile benchmark/ .yardopts
|
29
|
+
]
|
30
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
31
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*paths_to_exclude)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
spec.bindir = "exe"
|
35
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
36
|
+
spec.require_paths = ["lib"]
|
37
|
+
|
38
|
+
spec.add_dependency "pg", "~> 1.5"
|
39
|
+
spec.add_dependency "connection_pool", "~> 2.4"
|
40
|
+
end
|