changebase 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcaf7190bcd66bca83640222cc52f2bae125b65b9008c26c9e8dac4c88c35226
4
- data.tar.gz: '099666dc5717843cf99230a8a22c51fed5568ef93b4aa550928621a29d8a3e72'
3
+ metadata.gz: cb5026dc3675467dc638006f5953c5ba3a1ec4f486e63ecd4066fd2a027356f2
4
+ data.tar.gz: e4d58239417908fec5572dfe5e99bfb6d48db046679c39621d632cdcba8951fc
5
5
  SHA512:
6
- metadata.gz: 0b3dc117e4516d95e353b2e4e4ad076020815e37c88a6c256bb40ca1633ff5a487b692ac49331a203e8c6f519c46523c20e231e1cee768078c007666dea23d35
7
- data.tar.gz: f0915103e19c8966675d21d40b7091d62eb4eb97f686b92fc98087fe8277e3ef94ace4a3524ded170ead1cbd4f6d06395a03b6737fd937ca20edf4365a3225c3
6
+ metadata.gz: 5ed3fa5539602e0417635ff5333ec804a749930db38802a6f56636363b91a9896b1aa7acb18c0665471e9f66863e54e391a644d34e016014dda5090247eed488
7
+ data.tar.gz: 0cae8f40c3bc961c6381c0e4b9773b19d408e3602cbf47340d77479cc34c0bbb7827726f030b5929d3e466c944372b8f16d1792c0390f3f7696508ed88a53dd4
data/README.md CHANGED
@@ -47,27 +47,27 @@ Below are several diffent way of including metadata:
47
47
 
48
48
  ```ruby
49
49
  class ApplicationController < ActionController::Base
50
-
50
+
51
51
  # Just a block returning a hash of metadata.
52
52
  changebase do
53
53
  { my: data }
54
54
  end
55
-
55
+
56
56
  # Sets `release` in the metadata to the string RELEASE_SHA
57
57
  changebase :release, RELEASE_SHA
58
-
58
+
59
59
  # Sets `request_id` in the metadata to the value returned from the `Proc`
60
60
  changebase :request_id, -> { request.uuid }
61
-
61
+
62
62
  # Sets `user.id` in the metadata to the value returned from the
63
63
  # `current_user_id` function
64
64
  changebase :user, :id, :current_user_id
65
-
65
+
66
66
  # Sets `user.name` in the metadata to the value returned from the block
67
67
  changebase :user, :name do
68
68
  current_user.name
69
69
  end
70
-
70
+
71
71
  def current_user_id
72
72
  current_user.id
73
73
  end
@@ -99,8 +99,15 @@ To include metadata when creating or modifying data with ActiveRecord:
99
99
 
100
100
  ### Configuration
101
101
 
102
- To configure the metadata table that Changebase writes to create a initializer
103
- at `config/initializers/changebase.rb` with the following:
102
+ #### Replication Mode
103
+
104
+ The default mode for the `changebase` gem is `replication`. In this mode
105
+ Changebase is setup to replicate your database and record events via the
106
+ replication stream.
107
+
108
+ The default configuration `changebase` will write metadata to the
109
+ `"changebase_metadata"` table. To configure the metadata table create an
110
+ initializer at `config/initializers/changebase.rb` with the following:
104
111
 
105
112
  ```ruby
106
113
  Rails.application.config.tap do |config|
@@ -112,9 +119,67 @@ If you are not using Rails you can configure Changebase directly via:
112
119
 
113
120
  ```ruby
114
121
  Changebase.metadata_table = "my_very_cool_custom_metadata_table"
122
+
123
+ # Or
124
+
125
+ Changebase.configure(metadata_table: "my_very_cool_custom_metadata_table")
126
+ ```
127
+
128
+ #### Inline Mode
129
+
130
+ If you are unable to setup database replication you can use inline mode. Events
131
+ will be sent to through the Changebase API. You will collect roughly the same
132
+ information, but potentionally to miss events and changes in your database
133
+ if you are not careful, or if another application accesses the database directly.
134
+
135
+ ##### Limitations
136
+
137
+ - Any changes made to the database by ActiveRecord that does not first
138
+ instantiate the records will not be caputred. These methods include:
139
+ - [ActiveRecord::ConnectionAdapters::DatabaseStatements#execute](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-execute)
140
+ - [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_query](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_query)
141
+ - [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_update](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_update)
142
+ - [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_insert](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_insert)
143
+ - [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_delete](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_delete)
144
+ - [ActiveRecord::Persistence#delete](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-delete)
145
+ - [ActiveRecord::Persistence#update_column](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column)
146
+ - [ActiveRecord::Persistence#update_columns](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns)
147
+ - [ActiveRecord::Relation#touch_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all)
148
+ - [ActiveRecord::Relation#update_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all)
149
+ - [ActiveRecord::Relation#delete_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all)
150
+ - [ActiveRecord::Relation#delete_by](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by)
151
+ - Any changes made to the database outside of the Rails application will not be
152
+ captured.
153
+ - Ordering of events will not be guaranteed. The timestamp will be used as the
154
+ LSN, which may not be in the same order of transactions / events in the database.
155
+
156
+ To configure Changebase in the `"inline"` mode create a initializer at
157
+ `config/initializers/changebase.rb` with the following:
158
+
159
+ ```ruby
160
+ Rails.application.config.tap do |config|
161
+ config.changebase.mode = "inline"
162
+ config.changebase.connection = "https://#{ ENV.fetch('CHANGEBASE_API_KEY') }@changebase.io"
163
+ end
164
+ ```
165
+
166
+ If you are not using Rails you can configure Changebase directly via:
167
+
168
+ ```ruby
169
+ Changebase.configure do |config|
170
+ config.changebase.mode = "inline"
171
+ config.changebase.connection = "https://#{ ENV.fetch('CHANGEBASE_API_KEY') }@changebase.io"
172
+ end
173
+
174
+ # Or
175
+
176
+ Changebase.configure(
177
+ mode: "inline",
178
+ connection: "https://API_KEY@chanbase.io"
179
+ )
115
180
  ```
116
181
 
117
182
  ## Bugs
118
183
 
119
- If you think you found a bug, please file a ticket on the [issue
184
+ If you think you found a bug, please file a ticket on the [issue
120
185
  tracker](https://github.com/changebase-io/ruby-gem/issues).
@@ -4,7 +4,7 @@ module Changebase::ActionController
4
4
  included do
5
5
  prepend_around_action :changebase_metadata_wrapper
6
6
  end
7
-
7
+
8
8
  module ClassMethods
9
9
  def changebase(*keys, &block)
10
10
  method = if block
@@ -14,18 +14,18 @@ module Changebase::ActionController
14
14
  else
15
15
  keys.first
16
16
  end
17
-
17
+
18
18
  @changebase_metadata ||= []
19
19
  @changebase_metadata << [keys, method]
20
20
  end
21
-
21
+
22
22
  def changebase_metadata
23
23
  klass_metadata = if instance_variable_defined?(:@changebase_metadata)
24
24
  @changebase_metadata
25
25
  else
26
26
  []
27
27
  end
28
-
28
+
29
29
  if self.superclass.respond_to?(:changebase_metadata)
30
30
  klass_metadata + self.superclass.changebase_metadata
31
31
  else
@@ -33,21 +33,21 @@ module Changebase::ActionController
33
33
  end
34
34
  end
35
35
  end
36
-
36
+
37
37
  def changebase_metadata
38
38
  self.class.changebase_metadata
39
39
  end
40
-
40
+
41
41
  def changebase_metadata_wrapper(&block)
42
42
  metadata = {}
43
-
43
+
44
44
  changebase_metadata.each do |keys, value|
45
45
  data = metadata
46
46
  keys[0...-1].each do |key|
47
47
  data[key] ||= {}
48
48
  data = data[key]
49
49
  end
50
-
50
+
51
51
  value = case value
52
52
  when Symbol
53
53
  self.send(value)
@@ -56,7 +56,7 @@ module Changebase::ActionController
56
56
  else
57
57
  value
58
58
  end
59
-
59
+
60
60
  if keys.last
61
61
  data[keys.last] ||= value
62
62
  else
@@ -66,7 +66,7 @@ module Changebase::ActionController
66
66
 
67
67
  ActiveRecord::Base.with_metadata(metadata, &block)
68
68
  end
69
-
69
+
70
70
  end
71
71
 
72
- ActionController::Base.include(Changebase::ActionController)
72
+ ActionController::Base.include(Changebase::ActionController)
@@ -1,106 +1,23 @@
1
1
  module Changebase::ActiveRecord
2
2
  extend ActiveSupport::Concern
3
-
4
- module ClassMethods
3
+
4
+ class_methods do
5
5
  def with_metadata(metadata, &block)
6
6
  connection.with_metadata(metadata, &block)
7
7
  end
8
8
  end
9
-
10
- def with_metadata(metadata, &block)
11
- self.class.with_metadata(metadata, &block)
12
- end
13
- end
14
-
15
- module Changebase::ActiveRecord::Connection
16
9
 
17
10
  def with_metadata(metadata, &block)
18
- @changebase_metadata = metadata
19
- yield
20
- ensure
21
- @changebase_metadata = nil
22
- end
23
-
24
- end
25
-
26
- module Changebase::ActiveRecord::PostgreSQLAdapter
27
-
28
- def initialize(*args, **margs)
29
- @without_changebase = false
30
- @changebase_metadata = nil
31
- super
32
- end
33
-
34
- def without_changebase
35
- @without_changebase = true
36
- yield
37
- ensure
38
- @without_changebase = false
39
- end
40
-
41
- def drop_database(name) # :nodoc:
42
- without_changebase { super }
43
- end
44
-
45
- def drop_table(table_name, **options) # :nodoc:
46
- without_changebase { super }
47
- end
48
-
49
- def create_database(name, options = {})
50
- without_changebase { super }
51
- end
52
-
53
- def recreate_database(name, options = {}) # :nodoc:
54
- without_changebase { super }
55
- end
56
-
57
- def execute(sql, name = nil)
58
- if !@without_changebase && !current_transaction.open? && write_query?(sql)
59
- transaction { super }
60
- else
61
- super
62
- end
63
- end
64
-
65
- def exec_query(sql, name = "SQL", binds = [], prepare: false)
66
- if !@without_changebase && !current_transaction.open? && write_query?(sql)
67
- transaction { super }
68
- else
69
- super
70
- end
71
- end
72
-
73
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
74
- if !@without_changebase && !current_transaction.open? && write_query?(sql)
75
- transaction { super }
76
- else
77
- super
78
- end
11
+ self.class.with_metadata(metadata, &block)
79
12
  end
80
13
 
81
- def commit_db_transaction
82
- if !@without_changebase && @changebase_metadata && !@changebase_metadata.empty?
83
- sql = ActiveRecord::Base.send(:replace_named_bind_variables, <<~SQL, {version: 1, metadata: ActiveSupport::JSON.encode(@changebase_metadata)})
84
- INSERT INTO #{quote_table_name(Changebase.metadata_table)} ( version, data )
85
- VALUES ( :version, :metadata )
86
- ON CONFLICT ( version )
87
- DO UPDATE SET version = :version, data = :metadata;
88
- SQL
89
-
90
- log(sql, "CHANGEBASE") do
91
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
92
- @connection.async_exec(sql)
93
- end
94
- end
14
+ module Connection
15
+ def with_metadata(metadata, &block)
16
+ @changebase_metadata = metadata
17
+ yield
18
+ ensure
19
+ @changebase_metadata = nil
95
20
  end
96
- super
97
21
  end
98
22
 
99
- end
100
-
101
- require 'active_record'
102
- ActiveRecord::Base.include(Changebase::ActiveRecord)
103
- ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Changebase::ActiveRecord::Connection)
104
-
105
- require 'active_record/connection_adapters/postgresql_adapter'
106
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Changebase::ActiveRecord::PostgreSQLAdapter)
23
+ end
@@ -0,0 +1,399 @@
1
+ require 'net/https'
2
+
3
+ module Changebase
4
+
5
+ class ServerError < ::RuntimeError
6
+ end
7
+
8
+ # RuntimeErrors don't get translated by Rails into
9
+ # ActiveRecord::StatementInvalid which StandardError do. Would rather
10
+ # use StandardError, but it's usefull with Changebase to know when something
11
+ # raises a Changebase::Exception::NotFound or Forbidden
12
+ class Exception < ::RuntimeError
13
+
14
+ class UnexpectedResponse < Changebase::Exception
15
+ end
16
+
17
+ class BadRequest < Changebase::Exception
18
+ end
19
+
20
+ class Unauthorized < Changebase::Exception
21
+ end
22
+
23
+ class Forbidden < Changebase::Exception
24
+ end
25
+
26
+ class NotFound < Changebase::Exception
27
+ end
28
+
29
+ class Gone < Changebase::Exception
30
+ end
31
+
32
+ class MovedPermanently < Changebase::Exception
33
+ end
34
+
35
+ class BadGateway < Changebase::Exception
36
+ end
37
+
38
+ class ApiVersionUnsupported < Changebase::Exception
39
+ end
40
+
41
+ class ServiceUnavailable < Changebase::Exception
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+
49
+ # _Changebase::Connection_ is a low-level API. It provides basic HTTP #get,
50
+ # #post, #put, and #delete calls to the an HTTP(S) Server. It can also provides
51
+ # basic error checking of responses.
52
+ class Changebase::Connection
53
+
54
+ attr_reader :api_key, :host, :port, :use_ssl
55
+
56
+ # Initialize a connection Changebase.
57
+ #
58
+ # Options:
59
+ #
60
+ # * <tt>:url</tt> - An optional url used to set the protocol, host, port,
61
+ # and api_key
62
+ # * <tt>:host</tt> - The default is to connect to 127.0.0.1.
63
+ # * <tt>:port</tt> - Defaults to 80.
64
+ # * <tt>:use_ssl</tt> - Defaults to true.
65
+ # * <tt>:api_key</tt> - An optional token to send in the `Api-Key` header
66
+ # * <tt>:user_agent</tt> - An optional string. Will be joined with other
67
+ # User-Agent info.
68
+ def initialize(config)
69
+ if config[:url]
70
+ uri = URI.parse(config.delete(:url))
71
+ config[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
72
+ config[:host] ||= uri.host
73
+ config[:port] ||= uri.port
74
+ config[:use_ssl] ||= (uri.scheme == 'https')
75
+ end
76
+
77
+ [:api_key, :host, :port, :use_ssl, :user_agent].each do |key|
78
+ self.instance_variable_set(:"@#{key}", config[key])
79
+ end
80
+
81
+ @connection = Net::HTTP.new(host, port)
82
+ @connection.max_retries = 0
83
+ @connection.open_timeout = 5
84
+ @connection.read_timeout = 30
85
+ @connection.write_timeout = 5
86
+ @connection.ssl_timeout = 5
87
+ @connection.keep_alive_timeout = 30
88
+ @connection.use_ssl = use_ssl
89
+ true
90
+ end
91
+
92
+ def connect!
93
+ @connection.start
94
+ end
95
+
96
+ def active?
97
+ @connection.active?
98
+ end
99
+
100
+ def reconnect!
101
+ disconnect!
102
+ connect!
103
+ end
104
+
105
+ def disconnect!
106
+ @connection.finish if @connection.active?
107
+ end
108
+
109
+ # Returns the User-Agent of the client. Defaults to:
110
+ # "Rubygems/changebase@GEM_VERSION Ruby@RUBY_VERSION-pPATCH_LEVEL RUBY_PLATFORM"
111
+ def user_agent
112
+ [
113
+ @user_agent,
114
+ "Rubygems/changebase@#{Changebase::VERSION}",
115
+ "Ruby@#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
116
+ RUBY_PLATFORM
117
+ ].compact.join(' ')
118
+ end
119
+
120
+ # Sends a Net::HTTPRequest to the server. The headers returned from
121
+ # Connection#request_headers are automatically added to the request.
122
+ # The appropriate error is raised if the response is not in the 200..299
123
+ # range.
124
+ #
125
+ # Paramaters::
126
+ #
127
+ # * +request+ - A Net::HTTPRequest to send to the server
128
+ # * +body+ - Optional, a String, IO Object, or a Ruby object which is
129
+ # converted into JSON and sent as the body
130
+ # * +block+ - An optional block to call with the +Net::HTTPResponse+ object.
131
+ #
132
+ # Return Value::
133
+ #
134
+ # Returns the return value of the <tt>&block</tt> if given, otherwise the
135
+ # response object (a Net::HTTPResponse)
136
+ #
137
+ # Examples:
138
+ #
139
+ # #!ruby
140
+ # connection.send_request(#<Net::HTTP::Get>) # => #<Net::HTTP::Response>
141
+ #
142
+ # connection.send_request(#<Net::HTTP::Get @path="/404">) # => raises Changebase::Exception::NotFound
143
+ #
144
+ # # this will still raise an exception if the response_code is not valid
145
+ # # and the block will not be called
146
+ # connection.send_request(#<Net::HTTP::Get>) do |response|
147
+ # # ...
148
+ # end
149
+ #
150
+ # # The following example shows how to stream a response:
151
+ # connection.send_request(#<Net::HTTP::Get>) do |response|
152
+ # response.read_body do |chunk|
153
+ # io.write(chunk)
154
+ # end
155
+ # end
156
+ def send_request(request, body=nil, &block)
157
+ request_headers.each { |k, v| request[k] = v }
158
+ request['Content-Type'] ||= 'application/json'
159
+
160
+ if body.is_a?(IO)
161
+ request['Transfer-Encoding'] = 'chunked'
162
+ request.body_stream = body
163
+ elsif body.is_a?(String)
164
+ request.body = body
165
+ elsif body
166
+ request.body = JSON.generate(body)
167
+ end
168
+
169
+ return_value = nil
170
+ begin
171
+ close_connection = false
172
+ @connection.request(request) do |response|
173
+ # if response['Deprecation-Notice']
174
+ # ActiveSupport::Deprecation.warn(response['Deprecation-Notice'])
175
+ # end
176
+
177
+ validate_response_code(response)
178
+
179
+ # Get the cookies
180
+ response.each_header do |key, value|
181
+ case key.downcase
182
+ when 'connection'
183
+ close_connection = (value == 'close')
184
+ end
185
+ end
186
+
187
+ if block_given?
188
+ return_value = yield(response)
189
+ else
190
+ return_value = response
191
+ end
192
+ end
193
+ @connection.finish if close_connection
194
+ end
195
+
196
+ return_value
197
+ end
198
+
199
+ # Send a GET request to +path+ via +Connection#send_request+.
200
+ # See +Connection#send_request+ for more details on how the response is
201
+ # handled.
202
+ #
203
+ # Paramaters::
204
+ #
205
+ # * +path+ - The +path+ on the server to GET to.
206
+ # * +params+ - Either a String, Hash, or Ruby Object that responds to
207
+ # #to_param. Appended on the URL as query params
208
+ # * +block+ - An optional block to call with the +Net::HTTPResponse+ object.
209
+ #
210
+ # Return Value::
211
+ #
212
+ # See +Connection#send_request+
213
+ #
214
+ # Examples:
215
+ #
216
+ # #!ruby
217
+ # connection.get('/example') # => #<Net::HTTP::Response>
218
+ #
219
+ # connection.get('/example', 'query=stuff') # => #<Net::HTTP::Response>
220
+ #
221
+ # connection.get('/example', {:query => 'stuff'}) # => #<Net::HTTP::Response>
222
+ #
223
+ # connection.get('/404') # => raises Changebase::Exception::NotFound
224
+ #
225
+ # connection.get('/act') do |response|
226
+ # # ...
227
+ # end
228
+ def get(path, params='', &block)
229
+ params ||= ''
230
+ request = Net::HTTP::Get.new(path + '?' + params.to_param)
231
+
232
+ send_request(request, nil, &block)
233
+ end
234
+
235
+ # Send a POST request to +path+ via +Connection#send_request+.
236
+ # See +Connection#send_request+ for more details on how the response is
237
+ # handled.
238
+ #
239
+ # Paramaters::
240
+ #
241
+ # * +path+ - The +path+ on the server to POST to.
242
+ # * +body+ - Optional, See +Connection#send_request+.
243
+ # * +block+ - Optional, See +Connection#send_request+
244
+ #
245
+ # Return Value::
246
+ #
247
+ # See +Connection#send_request+
248
+ #
249
+ # Examples:
250
+ #
251
+ # #!ruby
252
+ # connection.post('/example') # => #<Net::HTTP::Response>
253
+ #
254
+ # connection.post('/example', 'body') # => #<Net::HTTP::Response>
255
+ #
256
+ # connection.post('/example', #<IO Object>) # => #<Net::HTTP::Response>
257
+ #
258
+ # connection.post('/example', {:example => 'data'}) # => #<Net::HTTP::Response>
259
+ #
260
+ # connection.post('/404') # => raises Changebase::Exception::NotFound
261
+ #
262
+ # connection.post('/act') do |response|
263
+ # # ...
264
+ # end
265
+ def post(path, body=nil, &block)
266
+ request = Net::HTTP::Post.new(path)
267
+
268
+ send_request(request, body, &block)
269
+ end
270
+
271
+ # Send a PUT request to +path+ via +Connection#send_request+.
272
+ # See +Connection#send_request+ for more details on how the response is
273
+ # handled.
274
+ #
275
+ # Paramaters::
276
+ #
277
+ # * +path+ - The +path+ on the server to POST to.
278
+ # * +body+ - Optional, See +Connection#send_request+.
279
+ # * +block+ - Optional, See +Connection#send_request+
280
+ #
281
+ # Return Value::
282
+ #
283
+ # See +Connection#send_request+
284
+ #
285
+ # Examples:
286
+ #
287
+ # #!ruby
288
+ # connection.put('/example') # => #<Net::HTTP::Response>
289
+ #
290
+ # connection.put('/example', 'body') # => #<Net::HTTP::Response>
291
+ #
292
+ # connection.put('/example', #<IO Object>) # => #<Net::HTTP::Response>
293
+ #
294
+ # connection.put('/example', {:example => 'data'}) # => #<Net::HTTP::Response>
295
+ #
296
+ # connection.put('/404') # => raises Changebase::Exception::NotFound
297
+ #
298
+ # connection.put('/act') do |response|
299
+ # # ...
300
+ # end
301
+ def put(path, body=nil, *valid_response_codes, &block)
302
+ request = Net::HTTP::Put.new(path)
303
+
304
+ send_request(request, body, &block)
305
+ end
306
+
307
+ # Send a DELETE request to +path+ via +Connection#send_request+.
308
+ # See +Connection#send_request+ for more details on how the response is
309
+ # handled
310
+ #
311
+ # Paramaters::
312
+ #
313
+ # * +path+ - The +path+ on the server to POST to.
314
+ # * +block+ - Optional, See +Connection#send_request+
315
+ #
316
+ # Return Value::
317
+ #
318
+ # See +Connection#send_request+
319
+ #
320
+ # Examples:
321
+ #
322
+ # #!ruby
323
+ # connection.delete('/example') # => #<Net::HTTP::Response>
324
+ #
325
+ # connection.delete('/404') # => raises Changebase::Exception::NotFound
326
+ #
327
+ # connection.delete('/act') do |response|
328
+ # # ...
329
+ # end
330
+ def delete(path, &block)
331
+ request = Net::HTTP::Delete.new(path)
332
+
333
+ send_request(request, nil, &block)
334
+ end
335
+
336
+ private
337
+
338
+ def request_headers
339
+ headers = {}
340
+
341
+ headers['Accept'] = 'application/json'
342
+ headers['User-Agent'] = user_agent
343
+ headers['Api-Version'] = '0.2.0'
344
+ headers['Connection'] = 'keep-alive'
345
+ headers['Api-Key'] = api_key if api_key
346
+
347
+ headers
348
+ end
349
+
350
+ # Raise an Changebase::Exception based on the response_code, unless the
351
+ # response_code is include in the valid_response_codes Array
352
+ #
353
+ # Paramaters::
354
+ #
355
+ # * +response+ - The Net::HTTP::Response object
356
+ #
357
+ # Return Value::
358
+ #
359
+ # If an exception is not raised the +response+ is returned
360
+ #
361
+ # Examples:
362
+ #
363
+ # #!ruby
364
+ # connection.validate_response_code(<Net::HTTP::Response @code=200>) # => <Net::HTTP::Response @code=200>
365
+ #
366
+ # connection.validate_response_code(<Net::HTTP::Response @code=404>) # => raises Changebase::Exception::NotFound
367
+ #
368
+ # connection.validate_response_code(<Net::HTTP::Response @code=500>) # => raises Changebase::Exception
369
+ def validate_response_code(response)
370
+ code = response.code.to_i
371
+
372
+ if !(200..299).include?(code)
373
+ case code
374
+ when 400
375
+ raise Changebase::Exception::BadRequest, response.body
376
+ when 401
377
+ raise Changebase::Exception::Unauthorized, response.body
378
+ when 403
379
+ raise Changebase::Exception::Forbidden, response.body
380
+ when 404
381
+ raise Changebase::Exception::NotFound, response.body
382
+ when 410
383
+ raise Changebase::Exception::Gone, response.body
384
+ when 422
385
+ raise Changebase::Exception::ApiVersionUnsupported, response.body
386
+ when 503
387
+ raise Changebase::Exception::ServiceUnavailable, response.body
388
+ when 301
389
+ raise Changebase::Exception::MovedPermanently, response.body
390
+ when 502
391
+ raise Changebase::Exception::BadGateway, response.body
392
+ when 500..599
393
+ raise Changebase::ServerError, response.body
394
+ else
395
+ raise Changebase::Exception, response.body
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,161 @@
1
+ require 'securerandom'
2
+
3
+ module Changebase::Inline
4
+
5
+ module Through
6
+ extend ActiveSupport::Concern
7
+
8
+ def delete_records(records, method)
9
+ x = super
10
+
11
+ if method != :destroy
12
+ records.each do |record|
13
+ through_model = source_reflection.active_record
14
+
15
+ columns = through_model.columns.each_with_index.reduce([]) do |acc, (column, index)|
16
+ attr_type = through_model.type_for_attribute(column.name)
17
+ previous_value = attr_type.serialize(column.name == source_reflection.foreign_key ? record.id : owner.id)
18
+ acc << {
19
+ index: index,
20
+ identity: true,
21
+ name: column.name,
22
+ type: column.sql_type,
23
+ value: nil,
24
+ previous_value: previous_value
25
+ }
26
+ acc
27
+ end
28
+
29
+ transaction = through_model.connection.changebase_transaction || Changebase::Inline::Transaction.new(
30
+ timestamp: Time.current,
31
+ metadata: through_model.connection.instance_variable_get(:@changebase_metadata)
32
+ )
33
+
34
+ transaction.event!({
35
+ schema: columns[0].try(:[], :schema) || through_model.connection.current_schema,
36
+ table: through_model.table_name,
37
+ type: :delete,
38
+ columns: columns,
39
+ timestamp: Time.current
40
+ })
41
+
42
+ # Save the Changebase::Transaction if we are not in a transaction.
43
+ transaction.save! if !through_model.connection.changebase_transaction
44
+ end
45
+ end
46
+
47
+ x
48
+ end
49
+ end
50
+
51
+ module HasMany
52
+
53
+ def delete_or_nullify_all_records(method)
54
+ x = super
55
+ if method == :delete_all
56
+ target.each { |record| record.changebase_track(:delete) }
57
+ end
58
+ x
59
+ end
60
+
61
+ end
62
+
63
+ module ActiveRecord
64
+
65
+ module PostgreSQLAdapter
66
+
67
+ attr_reader :changebase_transaction
68
+
69
+ # Begins a transaction.
70
+ def begin_db_transaction
71
+ super
72
+ @changebase_transaction = Changebase::Inline::Transaction.new(
73
+ timestamp: Time.current,
74
+ metadata: @changebase_metadata
75
+ )
76
+ end
77
+
78
+ # Aborts a transaction.
79
+ def exec_rollback_db_transaction
80
+ super
81
+ ensure
82
+ @changebase_transaction = nil
83
+ end
84
+
85
+ # Commits a transaction.
86
+ def commit_db_transaction
87
+ @changebase_transaction&.save!
88
+ @changebase_transaction = nil
89
+ super
90
+ end
91
+ end
92
+
93
+ extend ActiveSupport::Concern
94
+
95
+ class_methods do
96
+ def self.extended(other)
97
+ other.after_create { changebase_track(:insert) }
98
+ other.after_update { changebase_track(:update) }
99
+ other.after_destroy { changebase_track(:delete) }
100
+ end
101
+
102
+ end
103
+
104
+ def changebase_tracking
105
+ if Changebase.configured?# && self.class.instance_variable_defined?(:@changebase)
106
+ # self.class.instance_variable_get(:@changebase)
107
+ {exclude: []}
108
+ end
109
+ end
110
+
111
+ def changebase_transaction
112
+ self.class.connection.changebase_transaction
113
+ end
114
+
115
+ def changebase_track(type)
116
+ return if !changebase_tracking
117
+ return if type == :update && self.previous_changes.empty?
118
+
119
+ # Go through each of the Model#attributes and grab the type from the
120
+ # Model#type_for_attribute(attr) to do the serialization, grab the
121
+ # column definition using Model#column_for_attribute(attr) to write the
122
+ # type, and use Model.columns.index(col) to grab the index of the column
123
+ # in the database.
124
+ columns = self.class.columns.each_with_index.reduce([]) do |acc, (column, index)|
125
+ identity = self.class.primary_key ? self.class.primary_key == column.name : true
126
+
127
+ attr_type = self.type_for_attribute(column.name)
128
+ value = self.attributes[column.name]
129
+ previous_value = self.previous_changes[column.name].try(:[], 0)
130
+
131
+ case type
132
+ when :update
133
+ previous_value ||= value
134
+ when :delete
135
+ previous_value ||= value
136
+ value = nil
137
+ end
138
+
139
+ acc << {
140
+ index: index,
141
+ identity: identity,
142
+ name: column.name,
143
+ type: column.sql_type,
144
+ value: attr_type.serialize(value),
145
+ previous_value: attr_type.serialize(previous_value)
146
+ }
147
+ acc
148
+ end
149
+
150
+ # Emit the event
151
+ changebase_transaction.event!({
152
+ schema: columns[0].try(:[], :schema) || self.class.connection.current_schema,
153
+ table: self.class.table_name,
154
+ type: type,
155
+ columns: columns,
156
+ timestamp: Time.current
157
+ })
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,25 @@
1
+ class Changebase::Inline::Event
2
+
3
+ attr_accessor :id, :database_id, :transaction_id, :type, :schema,
4
+ :table, :timestamp, :created_at, :columns
5
+
6
+ def initialize(attrs)
7
+ attrs.each { |k,v| self.send("#{k}=", v) }
8
+
9
+ self.columns ||= {}
10
+ end
11
+
12
+ def as_json
13
+ {
14
+ id: id,
15
+ transaction_id: transaction_id,
16
+ lsn: timestamp.utc.iso8601(3),
17
+ type: type,
18
+ schema: schema,
19
+ table: table,
20
+ timestamp: timestamp.utc.iso8601(3),
21
+ columns: columns.as_json
22
+ }.compact
23
+ end
24
+
25
+ end
@@ -0,0 +1,71 @@
1
+ require 'securerandom'
2
+
3
+ class Changebase::Inline::Transaction
4
+
5
+ attr_accessor :id, :metadata, :timestamp, :events
6
+
7
+ def initialize(attrs={})
8
+ attrs.each { |k,v| self.send("#{k}=", v) }
9
+
10
+ if id
11
+ @persisted = true
12
+ else
13
+ @persisted = false
14
+ @id ||= SecureRandom.uuid
15
+ end
16
+
17
+ @events ||= []
18
+ @timestamp ||= Time.now
19
+ @metadata ||= {}
20
+ end
21
+
22
+ def persisted?
23
+ @persisted
24
+ end
25
+
26
+ def event!(event_attributes)
27
+ event = Changebase::Inline::Event.new(event_attributes)
28
+ @events << event
29
+ event
30
+ end
31
+
32
+ def self.create!(attrs={})
33
+ transaction = self.new(attrs)
34
+ transaction.save!
35
+ transaction
36
+ end
37
+
38
+ def save!
39
+ persisted? ? _update : _create
40
+ end
41
+
42
+ def _update
43
+ return if events.empty?
44
+ events.delete_if { |a| a.diff.empty? }
45
+ payload = JSON.generate({events: events.as_json.map{ |json| json[:transaction_id] = id; json }})
46
+ Changebase.logger.debug("[Changebase] POST /events WITH #{payload}")
47
+ Changebase.connection.post('/events', payload)
48
+ @events = []
49
+ end
50
+
51
+ def _create
52
+ events.delete_if { |a| a.columns.empty? }
53
+ payload = JSON.generate({transaction: self.as_json})
54
+ Changebase.logger.debug("[Changebase] POST /transactions WITH #{payload}")
55
+ Changebase.connection.post('/transactions', payload)
56
+ @events = []
57
+ @persisted = true
58
+ end
59
+
60
+ def as_json
61
+ result = {
62
+ id: id,
63
+ lsn: timestamp.utc.iso8601(3),
64
+ timestamp: timestamp.utc.iso8601(3),
65
+ events: events.as_json
66
+ }
67
+ result[:metadata] = metadata.as_json if !metadata.empty?
68
+ result
69
+ end
70
+
71
+ end
@@ -0,0 +1,32 @@
1
+ require 'changebase'
2
+ Changebase.mode = 'inline'
3
+
4
+ module Changebase::Inline
5
+
6
+ autoload :Event, 'changebase/inline/event'
7
+ autoload :Transaction, 'changebase/inline/transaction'
8
+
9
+ def self.load!
10
+ require 'active_record'
11
+ require 'changebase/active_record'
12
+
13
+ ::ActiveRecord::Base.include(Changebase::ActiveRecord)
14
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Changebase::ActiveRecord::Connection)
15
+
16
+ require 'active_record/connection_adapters/postgresql_adapter'
17
+ require 'changebase/inline/active_record'
18
+ ::ActiveRecord::Base.include(Changebase::Inline::ActiveRecord)
19
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Changebase::Inline::ActiveRecord::PostgreSQLAdapter)
20
+ ::ActiveRecord::Associations::HasManyThroughAssociation.prepend(Changebase::Inline::Through)
21
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(Changebase::Inline::HasMany)
22
+
23
+ @loaded = true
24
+ end
25
+
26
+ def self.loaded?
27
+ @loaded
28
+ end
29
+
30
+ end
31
+
32
+ Changebase::Inline.load!
@@ -1,23 +1,31 @@
1
1
  class Changebase::Engine < ::Rails::Engine
2
2
 
3
3
  config.changebase = ActiveSupport::OrderedOptions.new
4
+ # config.changebase.mode = nil
4
5
  config.changebase.metadata_table = "changebase_metadata"
5
-
6
+
6
7
  initializer :changebase do |app|
7
8
  migration_paths = config.paths['db/migrate'].expanded
8
-
9
+
9
10
  ActiveSupport.on_load(:active_record) do
10
- require 'changebase/active_record'
11
- migration_paths.each do |path|
12
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths << path
11
+ Changebase.logger = ActiveRecord::Base.logger
12
+
13
+ case Changebase.mode
14
+ when 'replication'
15
+ Changebase::Replication.load! if !Changebase::Replication.loaded?
16
+ migration_paths.each do |path|
17
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths << path
18
+ end
19
+ when 'inline'
20
+ Changebase::Inline.load! if !Changebase::Inline.loaded?
13
21
  end
14
22
  end
15
-
23
+
16
24
  ActiveSupport.on_load(:action_controller) do
17
25
  require 'changebase/action_controller'
18
26
  end
19
-
20
- Changebase.metadata_table = app.config.changebase.metadata_table
27
+
28
+ Changebase.configure(**app.config.changebase.to_h)
21
29
  end
22
-
23
- end
30
+
31
+ end
@@ -0,0 +1,96 @@
1
+ module Changebase::Replication
2
+ module ActiveRecord
3
+ module PostgreSQLAdapter
4
+
5
+ def initialize(*args, **margs)
6
+ @without_changebase = false
7
+ @changebase_metadata = nil
8
+ super
9
+ end
10
+
11
+ def without_changebase
12
+ @without_changebase = true
13
+ yield
14
+ ensure
15
+ @without_changebase = false
16
+ end
17
+
18
+ def drop_database(name) # :nodoc:
19
+ without_changebase { super }
20
+ end
21
+
22
+ def drop_table(table_name, **options) # :nodoc:
23
+ without_changebase { super }
24
+ end
25
+
26
+ def create_database(name, options = {})
27
+ without_changebase { super }
28
+ end
29
+
30
+ def recreate_database(name, options = {}) # :nodoc:
31
+ without_changebase { super }
32
+ end
33
+
34
+ def execute(sql, name = nil)
35
+ if !@without_changebase && !current_transaction.open? && write_query?(sql)
36
+ transaction { super }
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
43
+ if !@without_changebase && !current_transaction.open? && write_query?(sql)
44
+ transaction { super }
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def exec_delete(sql, name = nil, binds = []) # :nodoc:
51
+ if !@without_changebase && !current_transaction.open? && write_query?(sql)
52
+ transaction { super }
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def commit_db_transaction
59
+ if !@without_changebase && @changebase_metadata && !@changebase_metadata.empty?
60
+ sql = ::ActiveRecord::Base.send(:replace_named_bind_variables, <<~SQL, {version: 1, metadata: ActiveSupport::JSON.encode(@changebase_metadata)})
61
+ INSERT INTO #{quote_table_name(Changebase.metadata_table)} ( version, data )
62
+ VALUES ( :version, :metadata )
63
+ ON CONFLICT ( version )
64
+ DO UPDATE SET version = :version, data = :metadata;
65
+ SQL
66
+
67
+ log(sql, "CHANGEBASE") do
68
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
69
+ @connection.async_exec(sql)
70
+ end
71
+ end
72
+ end
73
+ super
74
+ end
75
+
76
+ if ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0")
77
+ CHANGEBASE_COMMENT_REGEX = %r{(?:--.*\n)|/\*(?:[^*]|\*[^/])*\*/}m
78
+ def self.changebase_build_read_query_regexp(*parts) # :nodoc:
79
+ parts += [:begin, :commit, :explain, :release, :rollback, :savepoint, :select, :with]
80
+ parts = parts.map { |part| /#{part}/i }
81
+ /\A(?:[(\s]|#{CHANGEBASE_COMMENT_REGEX})*#{Regexp.union(*parts)}/
82
+ end
83
+
84
+ CHANGEBASE_READ_QUERY = changebase_build_read_query_regexp(
85
+ :close, :declare, :fetch, :move, :set, :show
86
+ )
87
+ def write_query?(sql)
88
+ !CHANGEBASE_READ_QUERY.match?(sql)
89
+ rescue ArgumentError # Invalid encoding
90
+ !CHANGEBASE_READ_QUERY.match?(sql.b)
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,24 @@
1
+ require 'changebase'
2
+ Changebase.mode = 'replication'
3
+
4
+ module Changebase::Replication
5
+ def self.load!
6
+ require 'active_record'
7
+
8
+ ::ActiveRecord::Base.include(Changebase::ActiveRecord)
9
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Changebase::ActiveRecord::Connection)
10
+
11
+ require 'active_record/connection_adapters/postgresql_adapter'
12
+ require 'changebase/replication/active_record'
13
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Changebase::Replication::ActiveRecord::PostgreSQLAdapter)
14
+
15
+ @loaded = true
16
+ end
17
+
18
+ def self.loaded?
19
+ @loaded
20
+ end
21
+
22
+ end
23
+
24
+ Changebase::Replication.load!
@@ -1,3 +1,3 @@
1
1
  module Changebase
2
- VERSION = '0.1'
2
+ VERSION = '0.2'
3
3
  end
data/lib/changebase.rb CHANGED
@@ -1,16 +1,67 @@
1
1
  module Changebase
2
+
3
+ autoload :VERSION, 'changebase/version'
4
+ autoload :Connection, 'changebase/connection'
5
+ autoload :Inline, 'changebase/inline'
6
+ autoload :Replication, 'changebase/replication'
2
7
  autoload :ActiveRecord, 'changebase/active_record'
3
8
  autoload :ActionController, 'changebase/action_controller'
4
-
5
- @metadata_table = "changebase_metadata"
6
-
9
+
10
+ @config = {
11
+ mode: "replication",
12
+ metadata_table: "changebase_metadata"
13
+ }
14
+
7
15
  def self.metadata_table=(value)
8
- @metadata_table = value
16
+ @config[:metadata_table] = value
9
17
  end
10
-
18
+
11
19
  def self.metadata_table
12
- @metadata_table
20
+ @config[:metadata_table]
21
+ end
22
+
23
+ def self.mode=(value)
24
+ @config[:mode] = value
25
+ end
26
+
27
+ def self.mode
28
+ @config[:mode]
29
+ end
30
+
31
+ def self.connection=(value)
32
+ @config[:connection] = value
33
+ end
34
+
35
+ def self.connection
36
+ Thread.current[:changebase_connection] ||= Changebase::Connection.new({
37
+ url: @config[:connection]
38
+ })
39
+ end
40
+
41
+ def self.configure(**config)
42
+ @config.merge!(config)
43
+ self.logger = @config[:logger] if @config[:logger]
13
44
  end
45
+
46
+ def self.configured?
47
+ case @config[:mode]
48
+ when 'inline'
49
+ !!@config[:connection]
50
+ else
51
+ true
52
+ end
53
+ end
54
+
55
+ def self.logger
56
+ return @logger if defined?(@logger)
57
+
58
+ @logger = Logger.new(STDOUT)
59
+ end
60
+
61
+ def self.logger=(logger)
62
+ @logger = logger
63
+ end
64
+
14
65
  end
15
66
 
16
- require 'changebase/railtie' if defined?(Rails)
67
+ require 'changebase/railtie' if defined?(Rails)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: changebase
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-02-17 00:00:00.000000000 Z
12
+ date: 2022-06-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -17,7 +17,7 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '6'
20
+ version: '5.2'
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
23
  version: '8'
@@ -27,7 +27,7 @@ dependencies:
27
27
  requirements:
28
28
  - - ">="
29
29
  - !ruby/object:Gem::Version
30
- version: '6'
30
+ version: '5.2'
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '8'
@@ -37,7 +37,7 @@ dependencies:
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '6'
40
+ version: '5.2'
41
41
  - - "<"
42
42
  - !ruby/object:Gem::Version
43
43
  version: '8'
@@ -47,7 +47,7 @@ dependencies:
47
47
  requirements:
48
48
  - - ">="
49
49
  - !ruby/object:Gem::Version
50
- version: '6'
50
+ version: '5.2'
51
51
  - - "<"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '8'
@@ -57,7 +57,7 @@ dependencies:
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '6'
60
+ version: '5.2'
61
61
  - - "<"
62
62
  - !ruby/object:Gem::Version
63
63
  version: '8'
@@ -67,7 +67,7 @@ dependencies:
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: '6'
70
+ version: '5.2'
71
71
  - - "<"
72
72
  - !ruby/object:Gem::Version
73
73
  version: '8'
@@ -141,6 +141,20 @@ dependencies:
141
141
  - - ">="
142
142
  - !ruby/object:Gem::Version
143
143
  version: '0'
144
+ - !ruby/object:Gem::Dependency
145
+ name: webmock
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
144
158
  - !ruby/object:Gem::Dependency
145
159
  name: mocha
146
160
  requirement: !ruby/object:Gem::Requirement
@@ -225,7 +239,14 @@ files:
225
239
  - lib/changebase.rb
226
240
  - lib/changebase/action_controller.rb
227
241
  - lib/changebase/active_record.rb
242
+ - lib/changebase/connection.rb
243
+ - lib/changebase/inline.rb
244
+ - lib/changebase/inline/active_record.rb
245
+ - lib/changebase/inline/event.rb
246
+ - lib/changebase/inline/transaction.rb
228
247
  - lib/changebase/railtie.rb
248
+ - lib/changebase/replication.rb
249
+ - lib/changebase/replication/active_record.rb
229
250
  - lib/changebase/version.rb
230
251
  homepage: https://changebase.io
231
252
  licenses: []