table_sync 0.0.0 → 1.4.0

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: 415c63b858b66466938250116f5119c6e1c1889bd66e013d80f5c129348325cf
4
- data.tar.gz: 8f721b8f02f89361d76c363374cdf3879d860a0e6604286d89d994ba0463f707
3
+ metadata.gz: 837e9058c5d54a0c4fe0e99d91289997f50014b54829bef3a88cb8a690b84306
4
+ data.tar.gz: 73cee0ef5ebb441170a3170ae95591ffe8bdadd0b145cbe35ab9203ba71ca126
5
5
  SHA512:
6
- metadata.gz: 879f1e9daf37a2047cacbb7dc4d6ae0131bd87dba2468206882c3793e4d2f0ffe65cb48cc600e9cd97478b18d3bbe97d0ccfedf9f90555dba9ebc366edc1b059
7
- data.tar.gz: 55646503747e75b0acf98cab6c6274cd54b1c662523342c58680282258970986b696ff4bdd2c7279409dff904f36760df7e7a871f3b8d55fdfb4eecdd3974b80
6
+ metadata.gz: 7225eac461cbe364aa098ccadc77ddd710d529dccc5464d1d27818de98ece66ec65389ff4a5f2f8ae05164130d4afe1607f7288e76cc4b7adc8e0d9db3cd8b39
7
+ data.tar.gz: a145ad745bb10f14b6964b94403cb901d536a0f82c091b3bb54dd776989808343acc9ca901b27063680e8f86f8d368e061ced59f838a049fbbdf3592c0de55f3
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /tmp/
9
9
  .ruby-version
10
10
  Gemfile.lock
11
+ /log/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ --require spec_helper
data/.travis.yml CHANGED
@@ -10,8 +10,18 @@ matrix:
10
10
  allow_failures:
11
11
  - rvm: ruby-head
12
12
  sudo: false
13
+ dist: xenial
13
14
  cache: bundler
15
+ services:
16
+ - postgresql
17
+ addons:
18
+ postgresql: "10"
14
19
  before_install: gem install bundler
20
+ before_script:
21
+ - psql -c 'create database table_sync_test;' -U postgres
15
22
  script:
23
+ - mkdir log
24
+ - bundle exec rake bundle:audit
16
25
  - bundle exec rubocop
17
26
  - bundle exec rspec
27
+
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # TableSync · [![Gem Version](https://badge.fury.io/rb/table_sync.svg)](https://badge.fury.io/rb/table_sync) [![Build Status](https://travis-ci.org/umbrellio/table_sync.svg?branch=master)](https://travis-ci.org/umbrellio/table_sync) [![Coverage Status](https://coveralls.io/repos/github/umbrellio/table_sync/badge.svg?branch=master)](https://coveralls.io/github/umbrellio/table_sync?branch=master)
2
2
 
3
+ DB Table synchronization between microservices based on Model's event system and RabbitMQ messaging
4
+
3
5
  ## Instalation
4
6
 
5
7
  ```ruby
@@ -14,7 +16,7 @@ $ gem install table_sync
14
16
 
15
17
  ## Usage
16
18
 
17
- Coming soon...
19
+ - [Documentation](docs/synopsis.md)
18
20
 
19
21
  ## Contributing
20
22
 
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "bundler/audit/task"
4
5
  require "rspec/core/rake_task"
5
6
  require "rubocop"
6
7
  require "rubocop-rspec"
@@ -16,5 +17,6 @@ RuboCop::RakeTask.new(:rubocop) do |t|
16
17
  end
17
18
 
18
19
  RSpec::Core::RakeTask.new(:rspec)
20
+ Bundler::Audit::Task.new
19
21
 
20
22
  task default: :rspec
data/docs/synopsis.md ADDED
@@ -0,0 +1,246 @@
1
+ # TableSync
2
+
3
+ Table synchronization via RabbitMQ
4
+
5
+ # Publishing changes
6
+
7
+ Include `TableSync.sync(self)` into a Sequel or ActiveRecord model. `:if` and `:unless` are
8
+ supported for Sequel and ActiveRecord
9
+
10
+ Functioning `Rails.cache` is required
11
+
12
+ Example:
13
+
14
+ ```ruby
15
+ class SomeModel < Sequel::Model
16
+ TableSync.sync(self, { if: -> (*) { some_code } })
17
+ end
18
+ ```
19
+
20
+ #### #attributes_for_sync
21
+
22
+ Models can implement `#attributes_for_sync` to override which attributes are published. If not
23
+ present, all attributes are published
24
+
25
+ #### #attrs_for_routing_key
26
+
27
+ Models can implement `#attrs_for_routing_key` to override which attributes are given to routing_key_callable. If not present, default attributes are given
28
+
29
+ #### #attrs_for_metadata
30
+
31
+ Models can implement `#attrs_for_metadata` to override which attributes are given to metadata_callable. If not present, default attributes are given
32
+
33
+ #### .table_sync_model_name
34
+
35
+ Models can implement `.table_sync_model_name` class method to override the model name used for
36
+ publishing events. Default is model class name
37
+
38
+ #### .table_sync_destroy_attributes(original_attributes)
39
+
40
+ Models can implement `.table_sync_destroy_attributes` class method to override the attributes
41
+ used for publishing destroy events. Default is object's primary key
42
+
43
+ ## Configuration
44
+
45
+ - `TableSync.publishing_job_class_callable` is a callable which should resolve to a ActiveJob
46
+ subclass that calls TableSync back to actually publish changes (required)
47
+
48
+ Example:
49
+
50
+ ```ruby
51
+ class TableSync::Job < ActiveJob::Base
52
+ def perform(*args)
53
+ TableSync::Publisher.new(*args).publish_now
54
+ end
55
+ end
56
+ ```
57
+
58
+ - `TableSync.batch_publishing_job_class_callable` is a callable which should resolve to a ActiveJob
59
+ subclass that calls TableSync batch publisher back to actually publish changes (required for batch publisher)
60
+
61
+ - `TableSync.routing_key_callable` is a callable which resolves which routing key to use when
62
+ publishing changes. It receives object class and attributes (required)
63
+
64
+ Example:
65
+
66
+ ```ruby
67
+ TableSync.routing_key_callable = -> (klass, attributes) { klass.gsub('::', '_').tableize }
68
+ ```
69
+
70
+ - `TableSync.routing_metadata_callable` is a callable that adds RabbitMQ headers which can be
71
+ used in routing (optional). One possible way of using it is defining a headers exchange and
72
+ routing rules based on key-value pairs (which correspond to sent headers)
73
+
74
+ Example:
75
+
76
+ ```ruby
77
+ TableSync.routing_metadata_callable = -> (klass, attributes) { attributes.slice("project_id") }
78
+ ```
79
+
80
+ - `TableSync.exchange_name` defines the exchange name used for publishing (optional, falls back
81
+ to default Rabbit gem configuration).
82
+
83
+ # Manual publishing
84
+
85
+ `TableSync::Publisher.new(object_class, original_attributes, confirm: true, state: :updated)` where
86
+ state is one of `:created / :updated / :destroyed` and `confirm` is Rabbit's confirm delivery flag
87
+
88
+ # Manual publishing with batches
89
+
90
+ You can use `TableSync::BatchPublisher` to publish changes in batches (array of hashes in `attributes`).
91
+ For now, only the following changes in the table can be published: `create` and` update`.
92
+
93
+ When using `TableSync::BatchPublisher`,` TableSync.routing_key_callable` is called as follows:
94
+ `TableSync.routing_key_callable.call(klass, {})`, i.e. empty hash is passed instead of attributes.
95
+ And `TableSync.routing_metadata_callable` is not called at all: header value is set to empty hash.
96
+
97
+ `TableSync::BatchPublisher.new(object_class, original_attributes_array, **options)`, where `original_attributes_array` is an array with hash of attributes of published objects and `options` is a hash of options.
98
+
99
+ `options` consists of:
100
+ - `confirm`, which is a flag for RabbitMQ, `true` by default
101
+ - `routing_key`, which is a custom key used (if given) to override one from `TableSync.routing_key_callable`, `nil` by default
102
+ - `push_original_attributes` (default value is `false`), if this option is set to `true`,
103
+ original_attributes_array will be pushed to Rabbit instead of fetching records from database and sending their mapped attributes.
104
+
105
+ Example:
106
+
107
+ ```ruby
108
+ TableSync::BatchPublisher.new("SomeClass", [{ id: 1 }, { id: 2 }], confirm: false, routing_key: "custom_routing_key")
109
+ ```
110
+
111
+ # Manual publishing with batches (Russian)
112
+
113
+ С помощью класса `TableSync::BatchPublisher` вы можете опубликовать изменения батчами (массивом в `attributes`).
114
+ Пока можно публиковать только следующие изменения в таблице: `создание записи` и `обновление записи`.
115
+
116
+ При использовании `TableSync::BatchPublisher`, `TableSync.routing_key_callable` вызывается следующим образом:
117
+ `TableSync.routing_key_callable.call(klass, {})`, то есть вместо аттрибутов передается пустой хэш.
118
+ А `TableSync.routing_metadata_callable` не вызывается вовсе: в хидерах устанавливается пустой хэш.
119
+
120
+ `TableSync::BatchPublisher.new(object_class, original_attributes_array, **options)`, где `original_attributes_array` - массив с аттрибутами публикуемых объектов и `options`- это хэш с дополнительными опциями.
121
+
122
+ `options` состоит из:
123
+ - `confirm`, флаг для RabbitMQ, по умолчанию - `true`
124
+ - `routing_key`, ключ, который (если указан) замещает ключ, получаемый из `TableSync.routing_key_callable`, по умолчанию - `nil`
125
+ - `push_original_attributes` (значение по умолчанию `false`), если для этой опции задано значение true, в Rabbit будут отправлены original_attributes_array, вместо получения значений записей из базы непосредственно перед отправкой.
126
+
127
+ Example:
128
+
129
+ ```ruby
130
+ TableSync::BatchPublisher.new("SomeClass", [{ id: 1 }, { id: 2 }], confirm: false, routing_key: "custom_routing_key")
131
+ ```
132
+
133
+ # Receiving changes
134
+
135
+ Naming convention for receiving handlers is `Rabbit::Handler::GROUP_ID::TableSync`,
136
+ where `GROUP_ID` represents first part of source exchange name.
137
+ Define handler class inherited from `TableSync::ReceivingHandler`
138
+ and named according to described convention.
139
+ You should use DSL inside the class.
140
+ Suppose we will synchronize models {Project, News, User} project {MainProject}, then:
141
+
142
+ ```ruby
143
+ class Rabbit::Handler::MainProject::TableSync < TableSync::ReceivingHandler
144
+ queue_as :custom_queue
145
+
146
+ receive "Project", to_table: :projects
147
+
148
+ receive "News", to_table: :news, events: :update do
149
+ after_commit on: :update do
150
+ NewsCache.reload
151
+ end
152
+ end
153
+
154
+ receive "User", to_table: :clients, events: %i[update destroy] do
155
+ mapping_overrides email: :project_user_email, id: :project_user_id
156
+
157
+ only :project_user_email, :project_user_id
158
+ target_keys :project_id, :project_user_id
159
+ rest_key :project_user_rest
160
+ version_key :project_user_version
161
+
162
+ additional_data do |project_id:|
163
+ { project_id: project_id }
164
+ end
165
+
166
+ default_values do
167
+ { created_at: Time.current }
168
+ end
169
+ end
170
+
171
+ receive "User", to_table: :users do
172
+ rest_key nil
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### Handler class (`Rabbit::Handler::MainProject::TableSync`)
178
+
179
+ In this case:
180
+ - `TableSync` - RabbitMQ event type.
181
+ - `MainProject` - event source.
182
+ - `Rabbit::Handler` - module for our handlers of events from RabbitMQ (there might be others)
183
+
184
+ Method `queue_as` allow you to set custom queue.
185
+
186
+ ### Recieving handler batch processing
187
+
188
+ Receiving handler supports array of attributes in a single update event. Corresponding
189
+ upsert-style logic in ActiveRecord and Sequel orm handlers is provided.
190
+
191
+ ### Config DSL
192
+ ```ruby
193
+ receive source, to_table:, [events:, &block]
194
+ ```
195
+
196
+ The method receives following arguments
197
+ - `source` - string, name of source model (required)
198
+ - `to_table` - destination_table hash (required)
199
+ - `events` - array of supported events (optional)
200
+ - `block` - configuration block (optional)
201
+
202
+ This method implements logic of mapping `source` to `to_table` and allows customizing the event handling logic with provided block.
203
+ You can use one `source` for a lot of `to_table`.
204
+
205
+ The following options are available inside the block:
206
+ - `only` - whitelist for receiving attributes
207
+ - `skip` - return truthy value to skip the row
208
+ - `target_keys` - primary keys or unique keys
209
+ - `rest_key` - name of jsonb column for attributes which are not included in the whitelist. You can set the `rest_key(false)` or `rest_key(nil)` if you won't need the rest data.
210
+ - `version_key` - name of version column
211
+ - `first_sync_time_key` - name of the column where the time of first record synchronization should be stored. Disabled by default.
212
+ - `mapping_overrides` - map for overriding receiving columns
213
+ - `additional_data` - additional data for insert or update (e.g. `project_id`)
214
+ - `default_values` - values for insert if a row is not found
215
+ - `partitions` - proc that is used to obtain partitioned data to support table partitioning. Must return a hash which
216
+ keys are names of partitions of partitioned table and values - arrays of attributes to be inserted into particular
217
+ partition `{ measurements_2018_01: [ { attrs }, ... ], measurements_2018_02: [ { attrs }, ... ], ...}`.
218
+ While the proc is called inside an upsert transaction it is suitable place for creating partitions for new data.
219
+ Note that transaction of proc is a TableSynk.orm transaction.
220
+
221
+ ```ruby
222
+ partitions do |data:|
223
+ data.group_by { |d| "measurements_#{d[:time].year}_#{d[:time].month}" }
224
+ .tap { |data| data.keys.each { |table| DB.run("CREATE TABLE IF NOT EXISTS #{table} PARTITION OF measurements") } }
225
+ end
226
+ ```
227
+
228
+ Each of options can receive static value or code block which will be called for each event with the following arguments:
229
+ - `event` - type of event (`:update` or `:destroy`)
230
+ - `model` - source model (`Project`, `News`, `User` in example)
231
+ - `version` - version of the data
232
+ - `project_id` - id of project which is used in RabbitMQ
233
+ - `data` - raw data from event (before applying `mapping_overrides`, `only`, etc.)
234
+
235
+ Also, the `additional_data`, `skip` has a `current_row` field, which gives you a hash of all parameters of the current row (useful when receiving changes in batches).
236
+
237
+ Block can receive any number of parameters from the list.
238
+
239
+ ### Callbacks
240
+ You can set callbacks like this:
241
+ ```ruby
242
+ before_commit on: event, &block
243
+ after_commit on: event, &block
244
+ ```
245
+ TableSync performs this callbacks after transaction commit as to avoid side effects. Block receives array of
246
+ record attributes.
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TableSync::BasePublisher
4
+ include Memery
5
+
6
+ BASE_SAFE_JSON_TYPES = [NilClass, String, TrueClass, FalseClass, Numeric, Symbol].freeze
7
+ NOT_MAPPED = Object.new
8
+
9
+ private
10
+
11
+ attr_accessor :object_class
12
+
13
+ # @!method job_callable
14
+ # @!method job_callable_error_message
15
+ # @!method attrs_for_callables
16
+ # @!method attrs_for_routing_key
17
+ # @!method attrs_for_metadata
18
+ # @!method attributes_for_sync
19
+
20
+ memoize def current_time
21
+ Time.current
22
+ end
23
+
24
+ memoize def primary_keys
25
+ Array(object_class.primary_key).map(&:to_sym)
26
+ end
27
+
28
+ memoize def attributes_for_sync_defined?
29
+ object_class.method_defined?(:attributes_for_sync)
30
+ end
31
+
32
+ memoize def attrs_for_routing_key_defined?
33
+ object_class.method_defined?(:attrs_for_routing_key)
34
+ end
35
+
36
+ memoize def attrs_for_metadata_defined?
37
+ object_class.method_defined?(:attrs_for_metadata)
38
+ end
39
+
40
+ def resolve_routing_key
41
+ routing_key_callable.call(object_class.name, attrs_for_routing_key)
42
+ end
43
+
44
+ def metadata
45
+ TableSync.routing_metadata_callable&.call(object_class.name, attrs_for_metadata)
46
+ end
47
+
48
+ def confirm?
49
+ @confirm
50
+ end
51
+
52
+ def routing_key_callable
53
+ return TableSync.routing_key_callable if TableSync.routing_key_callable
54
+ raise "Can't publish, set TableSync.routing_key_callable"
55
+ end
56
+
57
+ def filter_safe_for_serialization(object)
58
+ case object
59
+ when Array
60
+ object.map(&method(:filter_safe_for_serialization)).select(&method(:object_mapped?))
61
+ when Hash
62
+ object
63
+ .transform_keys(&method(:filter_safe_for_serialization))
64
+ .transform_values(&method(:filter_safe_for_serialization))
65
+ .select { |*objects| objects.all?(&method(:object_mapped?)) }
66
+ when *BASE_SAFE_JSON_TYPES
67
+ object
68
+ else
69
+ NOT_MAPPED
70
+ end
71
+ end
72
+
73
+ def object_mapped?(object)
74
+ object != NOT_MAPPED
75
+ end
76
+
77
+ def job_class
78
+ job_callable ? job_callable.call : raise(job_callable_error_message)
79
+ end
80
+
81
+ def publishing_data
82
+ {
83
+ model: object_class.try(:table_sync_model_name) || object_class.name,
84
+ attributes: attributes_for_sync,
85
+ version: current_time.to_f,
86
+ }
87
+ end
88
+
89
+ def params
90
+ params = {
91
+ event: :table_sync,
92
+ data: publishing_data,
93
+ confirm_select: confirm?,
94
+ routing_key: routing_key,
95
+ realtime: true,
96
+ headers: metadata,
97
+ }
98
+
99
+ params[:exchange_name] = TableSync.exchange_name if TableSync.exchange_name
100
+
101
+ params
102
+ end
103
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TableSync::BatchPublisher < TableSync::BasePublisher
4
+ def initialize(object_class, original_attributes_array, **options)
5
+ @object_class = object_class.constantize
6
+ @original_attributes_array = original_attributes_array.map do |hash|
7
+ filter_safe_for_serialization(hash.deep_symbolize_keys)
8
+ end
9
+ @confirm = options[:confirm] || true
10
+ @routing_key = options[:routing_key] || resolve_routing_key
11
+ @push_original_attributes = options[:push_original_attributes] || false
12
+ end
13
+
14
+ def publish
15
+ enqueue_job
16
+ end
17
+
18
+ def publish_now
19
+ return unless need_publish?
20
+
21
+ Rabbit.publish(params)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :original_attributes_array, :routing_key
27
+
28
+ def push_original_attributes?
29
+ @push_original_attributes
30
+ end
31
+
32
+ def need_publish?
33
+ (push_original_attributes? && original_attributes_array.present?) || objects.present?
34
+ end
35
+
36
+ memoize def objects
37
+ needles.map { |needle| TableSync.orm.find(object_class, needle) }.compact
38
+ end
39
+
40
+ def job_callable
41
+ TableSync.batch_publishing_job_class_callable
42
+ end
43
+
44
+ def job_callable_error_message
45
+ "Can't publish, set TableSync.batch_publishing_job_class_callable"
46
+ end
47
+
48
+ def attrs_for_callables
49
+ {}
50
+ end
51
+
52
+ def attrs_for_routing_key
53
+ {}
54
+ end
55
+
56
+ def attr_for_metadata
57
+ {}
58
+ end
59
+
60
+ def params
61
+ {
62
+ **super,
63
+ headers: nil,
64
+ }
65
+ end
66
+
67
+ def needles
68
+ original_attributes_array.map { |original_attributes| original_attributes.slice(*primary_keys) }
69
+ end
70
+
71
+ def publishing_data
72
+ {
73
+ **super,
74
+ event: :update,
75
+ metadata: {},
76
+ }
77
+ end
78
+
79
+ def attributes_for_sync
80
+ return original_attributes_array if push_original_attributes?
81
+
82
+ objects.map do |object|
83
+ if attributes_for_sync_defined?
84
+ object.attributes_for_sync
85
+ else
86
+ TableSync.orm.attributes(object)
87
+ end
88
+ end
89
+ end
90
+
91
+ def enqueue_job
92
+ job_class.perform_later(
93
+ object_class.name,
94
+ original_attributes_array,
95
+ enqueue_additional_options,
96
+ )
97
+ end
98
+
99
+ def enqueue_additional_options
100
+ { confirm: confirm?, push_original_attributes: push_original_attributes? }
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TableSync::Config::CallbackRegistry
4
+ CALLBACK_KINDS = %i[after_commit before_commit].freeze
5
+ EVENTS = %i[create update destroy].freeze
6
+
7
+ InvalidCallbackKindError = Class.new(ArgumentError)
8
+ InvalidEventError = Class.new(ArgumentError)
9
+
10
+ def initialize
11
+ @callbacks = CALLBACK_KINDS.map { |event| [event, make_event_hash] }.to_h
12
+ end
13
+
14
+ def register_callback(callback, kind:, event:)
15
+ validate_callback_kind!(kind)
16
+ validate_event!(event)
17
+
18
+ callbacks.fetch(kind)[event] << callback
19
+ end
20
+
21
+ def get_callbacks(kind:, event:)
22
+ validate_callback_kind!(kind)
23
+ validate_event!(event)
24
+
25
+ callbacks.fetch(kind).fetch(event, [])
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :callbacks
31
+
32
+ def make_event_hash
33
+ Hash.new { |hsh, key| hsh[key] = [] }
34
+ end
35
+
36
+ def validate_callback_kind!(kind)
37
+ unless CALLBACK_KINDS.include?(kind)
38
+ raise(
39
+ InvalidCallbackKindError,
40
+ "Invalid callback kind: #{kind.inspect}. Valid kinds are #{CALLBACK_KINDS.inspect}",
41
+ )
42
+ end
43
+ end
44
+
45
+ def validate_event!(event)
46
+ unless EVENTS.include?(event)
47
+ raise(
48
+ InvalidEventError,
49
+ "Invalid event: #{event.inspect}. Valid events are #{EVENTS.inspect}",
50
+ )
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync
4
+ class Config
5
+ attr_reader :model, :events, :callback_registry
6
+
7
+ def initialize(model:, events: nil)
8
+ @model = model
9
+ @events = events.nil? ? nil : [events].flatten.map(&:to_sym)
10
+
11
+ @callback_registry = CallbackRegistry.new
12
+
13
+ only(model.columns)
14
+ mapping_overrides({})
15
+ additional_data({})
16
+ default_values({})
17
+ @rest_key = :rest
18
+ @version_key = :version
19
+ @first_sync_time_key = nil
20
+ target_keys(model.primary_keys)
21
+ end
22
+
23
+ # add_option implements next logic
24
+ # config.option - get value
25
+ # config.option(args) - set static value
26
+ # config.option { ... } - set proc as value
27
+ def self.add_option(name, &option_block)
28
+ ivar = "@#{name}".to_sym
29
+
30
+ option_block ||= proc { |value| value }
31
+
32
+ define_method(name) do |*args, &block|
33
+ if args.empty? && block.nil?
34
+ instance_variable_get(ivar)
35
+ elsif block
36
+ params = block.parameters.map { |param| param[0] == :keyreq ? param[1] : nil }.compact
37
+ unified_block = proc { |hash = {}| block.call(hash.slice(*params)) }
38
+ instance_variable_set(ivar, unified_block)
39
+ else
40
+ instance_variable_set(ivar, instance_exec(*args, &option_block))
41
+ end
42
+ end
43
+ end
44
+
45
+ def allow_event?(name)
46
+ return true if events.nil?
47
+ events.include?(name)
48
+ end
49
+
50
+ def before_commit(on:, &block)
51
+ callback_registry.register_callback(block, kind: :before_commit, event: on.to_sym)
52
+ end
53
+
54
+ def after_commit(on:, &block)
55
+ callback_registry.register_callback(block, kind: :after_commit, event: on.to_sym)
56
+ end
57
+
58
+ check_and_set_column_key = proc do |key|
59
+ key = key.to_sym
60
+ raise "#{model.inspect} doesn't have key: #{key}" unless model.columns.include?(key)
61
+ key
62
+ end
63
+
64
+ set_column_keys = proc do |*keys|
65
+ [keys].flatten.map { |k| instance_exec(k, &check_and_set_column_key) }
66
+ end
67
+
68
+ add_option(:only, &set_column_keys)
69
+ add_option(:target_keys, &set_column_keys)
70
+ add_option(:rest_key) do |value|
71
+ value ? instance_exec(value, &check_and_set_column_key) : nil
72
+ end
73
+ add_option(:version_key, &check_and_set_column_key)
74
+ add_option(:first_sync_time_key) do |value|
75
+ value ? instance_exec(value, &check_and_set_column_key) : nil
76
+ end
77
+
78
+ add_option(:mapping_overrides)
79
+ add_option(:additional_data)
80
+ add_option(:default_values)
81
+ add_option(:partitions)
82
+ add_option(:skip)
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync
4
+ # rubocop:disable Style/MissingRespondToMissing, Style/MethodMissingSuper
5
+ class ConfigDecorator
6
+ include ::TableSync::EventActions
7
+
8
+ def initialize(config, handler)
9
+ @config = config
10
+ @handler = handler
11
+ end
12
+
13
+ def method_missing(name, *args)
14
+ value = @config.send(name)
15
+
16
+ if value.is_a?(Proc)
17
+ value.call(
18
+ event: @handler.event,
19
+ model: @handler.model,
20
+ version: @handler.version,
21
+ project_id: @handler.project_id,
22
+ data: @handler.data,
23
+ current_row: args.first,
24
+ )
25
+ else
26
+ value
27
+ end
28
+ end
29
+
30
+ def allow_event?(event)
31
+ @config.allow_event?(event)
32
+ end
33
+ end
34
+ # rubocop:enable Style/MissingRespondToMissing, Style/MethodMissingSuper
35
+ end