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 +4 -4
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/README.md +3 -1
- data/Rakefile +2 -0
- data/docs/synopsis.md +246 -0
- data/lib/table_sync/base_publisher.rb +103 -0
- data/lib/table_sync/batch_publisher.rb +102 -0
- data/lib/table_sync/config/callback_registry.rb +53 -0
- data/lib/table_sync/config.rb +84 -0
- data/lib/table_sync/config_decorator.rb +35 -0
- data/lib/table_sync/dsl.rb +25 -0
- data/lib/table_sync/errors.rb +30 -0
- data/lib/table_sync/event_actions.rb +60 -0
- data/lib/table_sync/model/active_record.rb +110 -0
- data/lib/table_sync/model/sequel.rb +76 -0
- data/lib/table_sync/orm_adapter/active_record.rb +29 -0
- data/lib/table_sync/orm_adapter/sequel.rb +50 -0
- data/lib/table_sync/publisher.rb +123 -0
- data/lib/table_sync/receiving_handler.rb +76 -0
- data/lib/table_sync/version.rb +1 -1
- data/lib/table_sync.rb +51 -1
- data/table_sync.gemspec +15 -2
- metadata +148 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 837e9058c5d54a0c4fe0e99d91289997f50014b54829bef3a88cb8a690b84306
|
4
|
+
data.tar.gz: 73cee0ef5ebb441170a3170ae95591ffe8bdadd0b145cbe35ab9203ba71ca126
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7225eac461cbe364aa098ccadc77ddd710d529dccc5464d1d27818de98ece66ec65389ff4a5f2f8ae05164130d4afe1607f7288e76cc4b7adc8e0d9db3cd8b39
|
7
|
+
data.tar.gz: a145ad745bb10f14b6964b94403cb901d536a0f82c091b3bb54dd776989808343acc9ca901b27063680e8f86f8d368e061ced59f838a049fbbdf3592c0de55f3
|
data/.gitignore
CHANGED
data/.rspec
ADDED
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 · [](https://badge.fury.io/rb/table_sync) [](https://travis-ci.org/umbrellio/table_sync) [](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
|
-
|
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
|