pg_eventstore 0.1.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 +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
|