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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +90 -0
  6. data/db/extensions.sql +2 -0
  7. data/db/indexes.sql +13 -0
  8. data/db/primary_and_foreign_keys.sql +11 -0
  9. data/db/tables.sql +21 -0
  10. data/docs/appending_events.md +170 -0
  11. data/docs/configuration.md +82 -0
  12. data/docs/events_and_streams.md +45 -0
  13. data/docs/multiple_commands.md +46 -0
  14. data/docs/reading_events.md +161 -0
  15. data/docs/writing_middleware.md +160 -0
  16. data/lib/pg_eventstore/abstract_command.rb +18 -0
  17. data/lib/pg_eventstore/client.rb +133 -0
  18. data/lib/pg_eventstore/commands/append.rb +61 -0
  19. data/lib/pg_eventstore/commands/multiple.rb +14 -0
  20. data/lib/pg_eventstore/commands/read.rb +24 -0
  21. data/lib/pg_eventstore/commands.rb +6 -0
  22. data/lib/pg_eventstore/config.rb +30 -0
  23. data/lib/pg_eventstore/connection.rb +97 -0
  24. data/lib/pg_eventstore/errors.rb +107 -0
  25. data/lib/pg_eventstore/event.rb +59 -0
  26. data/lib/pg_eventstore/event_class_resolver.rb +17 -0
  27. data/lib/pg_eventstore/event_serializer.rb +27 -0
  28. data/lib/pg_eventstore/extensions/options_extension.rb +103 -0
  29. data/lib/pg_eventstore/middleware.rb +15 -0
  30. data/lib/pg_eventstore/pg_result_deserializer.rb +48 -0
  31. data/lib/pg_eventstore/queries.rb +127 -0
  32. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +187 -0
  33. data/lib/pg_eventstore/rspec/has_option_matcher.rb +90 -0
  34. data/lib/pg_eventstore/sql_builder.rb +126 -0
  35. data/lib/pg_eventstore/stream.rb +72 -0
  36. data/lib/pg_eventstore/tasks/setup.rake +37 -0
  37. data/lib/pg_eventstore/version.rb +5 -0
  38. data/lib/pg_eventstore.rb +85 -0
  39. data/pg_eventstore.gemspec +40 -0
  40. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ VERSION = "0.1.0"
5
+ 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