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 +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 · [![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
|
-
|
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
|