table_sync 0.0.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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