pg_eventstore 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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