table_sync 2.3.0 → 4.0.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/.rubocop.yml +21 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile.lock +82 -77
- data/README.md +4 -2
- data/docs/message_protocol.md +24 -0
- data/docs/notifications.md +45 -0
- data/docs/publishing.md +147 -0
- data/docs/receiving.md +341 -0
- data/lib/table_sync.rb +16 -31
- data/lib/table_sync/errors.rb +39 -23
- data/lib/table_sync/publishing.rb +11 -0
- data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
- data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +4 -4
- data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +3 -7
- data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +2 -6
- data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
- data/lib/table_sync/receiving.rb +14 -0
- data/lib/table_sync/receiving/config.rb +218 -0
- data/lib/table_sync/receiving/config_decorator.rb +27 -0
- data/lib/table_sync/receiving/dsl.rb +28 -0
- data/lib/table_sync/receiving/handler.rb +131 -0
- data/lib/table_sync/{model → receiving/model}/active_record.rb +36 -22
- data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
- data/lib/table_sync/utils.rb +9 -0
- data/lib/table_sync/utils/interface_checker.rb +97 -0
- data/lib/table_sync/utils/proc_array.rb +17 -0
- data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
- data/lib/table_sync/version.rb +1 -1
- data/table_sync.gemspec +2 -1
- metadata +42 -30
- data/docs/development.md +0 -43
- data/docs/synopsis.md +0 -336
- data/lib/table_sync/config.rb +0 -105
- data/lib/table_sync/config/callback_registry.rb +0 -53
- data/lib/table_sync/config_decorator.rb +0 -38
- data/lib/table_sync/dsl.rb +0 -25
- data/lib/table_sync/event_actions.rb +0 -96
- data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
- data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
- data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
- data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
- data/lib/table_sync/plugins.rb +0 -72
- data/lib/table_sync/plugins/abstract.rb +0 -55
- data/lib/table_sync/plugins/access_mixin.rb +0 -49
- data/lib/table_sync/plugins/registry.rb +0 -153
- data/lib/table_sync/receiving_handler.rb +0 -76
data/docs/receiving.md
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
# Receiving changes
|
2
|
+
|
3
|
+
Naming convention for receiving handlers is `Rabbit::Handler::GROUP_ID::TableSync`,
|
4
|
+
where `GROUP_ID` represents first part of source exchange name.
|
5
|
+
Define handler class inherited from `TableSync::ReceivingHandler`
|
6
|
+
and named according to described convention.
|
7
|
+
You should use DSL inside the class.
|
8
|
+
Suppose we will synchronize models {Project, News, User} project {MainProject}, then:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class Rabbit::Handler::MainProject::TableSync < TableSync::ReceivingHandler
|
12
|
+
queue_as :custom_queue
|
13
|
+
|
14
|
+
receive "Project", to_table: :projects
|
15
|
+
|
16
|
+
receive "News", to_table: :news, events: :update do
|
17
|
+
after_commit_on_update do
|
18
|
+
NewsCache.reload
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
receive "User", to_table: :clients, events: %i[update destroy] do
|
23
|
+
mapping_overrides email: :project_user_email, id: :project_user_id
|
24
|
+
|
25
|
+
only :project_user_email, :project_user_id
|
26
|
+
target_keys :project_id, :project_user_id
|
27
|
+
rest_key :project_user_rest
|
28
|
+
version_key :project_user_version
|
29
|
+
|
30
|
+
additional_data do |project_id:|
|
31
|
+
{ project_id: project_id }
|
32
|
+
end
|
33
|
+
|
34
|
+
default_values do
|
35
|
+
{ created_at: Time.current }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
receive "User", to_model: CustomModel.new(:users) do
|
40
|
+
rest_key nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### Handler class (`Rabbit::Handler::MainProject::TableSync`)
|
46
|
+
|
47
|
+
In this case:
|
48
|
+
- `TableSync` - RabbitMQ event type.
|
49
|
+
- `MainProject` - event source.
|
50
|
+
- `Rabbit::Handler` - module for our handlers of events from RabbitMQ (there might be others)
|
51
|
+
|
52
|
+
Method `queue_as` allows you to set custom queue.
|
53
|
+
|
54
|
+
### Recieving handler batch processing
|
55
|
+
|
56
|
+
Receiving handler supports array of attributes in a single update or destroy event. Corresponding
|
57
|
+
upsert-style logic in ActiveRecord and Sequel orm handlers are provided.
|
58
|
+
|
59
|
+
### Config
|
60
|
+
```ruby
|
61
|
+
receive source, [to_table:, to_model:, events:, &block]
|
62
|
+
```
|
63
|
+
|
64
|
+
The method receives following arguments
|
65
|
+
- `source` - string, name of source model (required)
|
66
|
+
- `to_table` - destination table name (required if not set to_model)
|
67
|
+
- `to_model` - destination model (required if not set to_table)
|
68
|
+
- `events` - array of supported events (optional)
|
69
|
+
- `block` - configuration block with options (optional)
|
70
|
+
|
71
|
+
This method implements logic of mapping `source` to `to_table` (or to `to_model`) and allows customizing
|
72
|
+
the event handling logic with provided block.
|
73
|
+
You can use one `source` for a lot of `to_table` or `to_moel`.
|
74
|
+
|
75
|
+
### Options:
|
76
|
+
|
77
|
+
Most of the options can be set as computed value or as a process.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
option(value)
|
81
|
+
```
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
option do |key params|
|
85
|
+
value
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
Each of options can receive static value or code block which will be called for each event with the following arguments:
|
90
|
+
- `event` - type of event (`:update` or `:destroy`)
|
91
|
+
- `model` - source model (`Project`, `News`, `User` in example)
|
92
|
+
- `version` - version of the data
|
93
|
+
- `project_id` - id of project which is used in RabbitMQ
|
94
|
+
- `raw_data` - raw data from event (before applying `mapping_overrides`, `only`, etc.)
|
95
|
+
|
96
|
+
Blocks can receive any number of parameters from the list.
|
97
|
+
|
98
|
+
All specific key params will be explained in examples for each option.
|
99
|
+
|
100
|
+
#### only
|
101
|
+
Whitelist for receiving attributes.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
only(instance of Array)
|
105
|
+
```
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
only do |row:|
|
109
|
+
return instance of Array
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
default value is taken through the call `model.columns`
|
114
|
+
|
115
|
+
#### target_keys
|
116
|
+
Primary keys or unique keys.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
target_keys(instance of Array)
|
120
|
+
```
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
target_keys do |data:|
|
124
|
+
return instance of Array
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
default value is taken through the call `model.primary_keys`
|
129
|
+
|
130
|
+
#### rest_key
|
131
|
+
Name of jsonb column for attributes which are not included in the whitelist.
|
132
|
+
You can set the `rest_key(false)` if you won't need the rest data.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
rest_key(instance of Symbol)
|
136
|
+
```
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
rest_key do |row:, rest:|
|
140
|
+
return instance of Symbol
|
141
|
+
end
|
142
|
+
```
|
143
|
+
default value is `:rest`
|
144
|
+
|
145
|
+
#### version_key
|
146
|
+
Name of version column.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
version_key(instance of Symbol)
|
150
|
+
```
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
version_key do |data:|
|
154
|
+
return instance of Symbol
|
155
|
+
end
|
156
|
+
```
|
157
|
+
default value is `:version`
|
158
|
+
|
159
|
+
#### except
|
160
|
+
Blacklist for receiving attributes.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
except(instance of Array)
|
164
|
+
```
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
except do |row:|
|
168
|
+
return instance of Array
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
default value is `[]`
|
173
|
+
|
174
|
+
#### mapping_overrides
|
175
|
+
Map for overriding receiving columns.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
mapping_overrides(instance of Hash)
|
179
|
+
```
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
mapping_overrides do |row:|
|
183
|
+
return instance of Hash
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
default value is `{}`
|
188
|
+
|
189
|
+
#### additional_data
|
190
|
+
Additional data for insert or update (e.g. `project_id`).
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
additional_data(instance of Hash)
|
194
|
+
```
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
additional_data do |row:|
|
198
|
+
return instance of Hash
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
default value is `{}`
|
203
|
+
|
204
|
+
#### default_values
|
205
|
+
Values for insert if a row is not found.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
default_values(instance of Hash)
|
209
|
+
```
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
default_values do |data:|
|
213
|
+
return instance of Hash
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
default value is `{}`
|
218
|
+
|
219
|
+
#### skip
|
220
|
+
Return truthy value to skip the row.
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
skip(instance of TrueClass or FalseClass)
|
224
|
+
```
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
skip do |data:|
|
228
|
+
return instance of TrueClass or FalseClass
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
default value is `false`
|
233
|
+
|
234
|
+
#### wrap_receiving
|
235
|
+
Proc that is used to wrap the receiving logic by custom block of code.
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
wrap_receiving do |data:, target_keys:, version_key:, default_values: {}, &receiving_logic|
|
239
|
+
receiving_logic.call
|
240
|
+
return makes no sense
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
default value is `proc { |&block| block.call }`
|
245
|
+
|
246
|
+
#### before_update
|
247
|
+
Perform code before updating data in the database.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
before_update do |data:, target_keys:, version_key:, default_values:|
|
251
|
+
return makes no sense
|
252
|
+
end
|
253
|
+
|
254
|
+
before_update do |data:, target_keys:, version_key:, default_values:|
|
255
|
+
return makes no sense
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
Сan be defined several times. Execution order guaranteed.
|
260
|
+
|
261
|
+
#### after_commit_on_update
|
262
|
+
Perform code after updated data was committed.
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
after_commit_on_update do |data:, target_keys:, version_key:, default_values:|
|
266
|
+
return makes no sense
|
267
|
+
end
|
268
|
+
|
269
|
+
after_commit_on_update do |data:, target_keys:, version_key:, default_values:|
|
270
|
+
return makes no sense
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
Сan be defined several times. Execution order guaranteed.
|
275
|
+
|
276
|
+
#### before_destroy
|
277
|
+
Perform code before destroying data in database.
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
before_destroy do |data:, target_keys:, version_key:|
|
281
|
+
return makes no sense
|
282
|
+
end
|
283
|
+
|
284
|
+
before_destroy do |data:, target_keys:, version_key:|
|
285
|
+
return makes no sense
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
Сan be defined several times. Execution order guaranteed.
|
290
|
+
|
291
|
+
#### after_commit_on_destroy
|
292
|
+
Perform code after destroyed data was committed.
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
after_commit_on_destroy do |data:, target_keys:, version_key:|
|
296
|
+
return makes no sense
|
297
|
+
end
|
298
|
+
|
299
|
+
after_commit_on_destroy do |data:, target_keys:, version_key:|
|
300
|
+
return makes no sense
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
Сan be defined several times. Execution order guaranteed.
|
305
|
+
|
306
|
+
### Custom model
|
307
|
+
You can use custom model for receiving.
|
308
|
+
```
|
309
|
+
class Rabbit::Handler::MainProject::TableSync < TableSync::ReceivingHandler
|
310
|
+
receive "Project", to_model: CustomModel.new
|
311
|
+
end
|
312
|
+
```
|
313
|
+
|
314
|
+
This model has to implement next interface:
|
315
|
+
```
|
316
|
+
def columns
|
317
|
+
return all columns from table
|
318
|
+
end
|
319
|
+
|
320
|
+
def primary_keys
|
321
|
+
return primary keys from table
|
322
|
+
end
|
323
|
+
|
324
|
+
def upsert(data: Array, target_keys: Array, version_key: Symbol, default_values: Hash)
|
325
|
+
return array with updated rows
|
326
|
+
end
|
327
|
+
|
328
|
+
def destroy(data: Array, target_keys: Array, version_key: Symbol)
|
329
|
+
return array with delited rows
|
330
|
+
end
|
331
|
+
|
332
|
+
def transaction(&block)
|
333
|
+
block.call
|
334
|
+
return makes no sense
|
335
|
+
end
|
336
|
+
|
337
|
+
def after_commit(&block)
|
338
|
+
block.call
|
339
|
+
return makes no sense
|
340
|
+
end
|
341
|
+
```
|
data/lib/table_sync.rb
CHANGED
@@ -1,66 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "memery"
|
4
|
+
require "self_data"
|
4
5
|
require "rabbit_messaging"
|
5
6
|
require "rabbit/event_handler" # NOTE: from rabbit_messaging"
|
6
7
|
require "active_support/core_ext/object/blank"
|
7
8
|
require "active_support/core_ext/numeric/time"
|
8
9
|
|
9
10
|
module TableSync
|
11
|
+
require_relative "table_sync/utils"
|
10
12
|
require_relative "table_sync/version"
|
11
13
|
require_relative "table_sync/errors"
|
12
|
-
require_relative "table_sync/plugins"
|
13
|
-
require_relative "table_sync/event_actions"
|
14
|
-
require_relative "table_sync/event_actions/data_wrapper"
|
15
|
-
require_relative "table_sync/config"
|
16
|
-
require_relative "table_sync/config/callback_registry"
|
17
|
-
require_relative "table_sync/config_decorator"
|
18
|
-
require_relative "table_sync/dsl"
|
19
|
-
require_relative "table_sync/receiving_handler"
|
20
|
-
require_relative "table_sync/base_publisher"
|
21
|
-
require_relative "table_sync/publisher"
|
22
|
-
require_relative "table_sync/batch_publisher"
|
23
|
-
require_relative "table_sync/orm_adapter/active_record"
|
24
|
-
require_relative "table_sync/orm_adapter/sequel"
|
25
|
-
require_relative "table_sync/model/active_record"
|
26
|
-
require_relative "table_sync/model/sequel"
|
27
14
|
require_relative "table_sync/instrument"
|
28
15
|
require_relative "table_sync/instrument_adapter/active_support"
|
29
16
|
require_relative "table_sync/naming_resolver/active_record"
|
30
17
|
require_relative "table_sync/naming_resolver/sequel"
|
31
|
-
|
32
|
-
|
33
|
-
# @since 2.2.0
|
34
|
-
extend Plugins::AccessMixin
|
18
|
+
require_relative "table_sync/receiving"
|
19
|
+
require_relative "table_sync/publishing"
|
35
20
|
|
36
21
|
class << self
|
37
|
-
include Memery
|
38
|
-
|
39
22
|
attr_accessor :publishing_job_class_callable
|
40
23
|
attr_accessor :batch_publishing_job_class_callable
|
41
24
|
attr_accessor :routing_key_callable
|
42
25
|
attr_accessor :exchange_name
|
43
26
|
attr_accessor :routing_metadata_callable
|
44
27
|
attr_accessor :notifier
|
28
|
+
attr_reader :orm
|
29
|
+
attr_reader :publishing_adapter
|
30
|
+
attr_reader :receiving_model
|
45
31
|
|
46
32
|
def sync(klass, **opts)
|
47
|
-
|
33
|
+
publishing_adapter.setup_sync(klass, opts)
|
48
34
|
end
|
49
35
|
|
50
36
|
def orm=(val)
|
51
|
-
|
52
|
-
@orm = val
|
53
|
-
end
|
54
|
-
|
55
|
-
memoize def orm
|
56
|
-
case @orm
|
37
|
+
case val
|
57
38
|
when :active_record
|
58
|
-
ORMAdapter::ActiveRecord
|
39
|
+
@publishing_adapter = Publishing::ORMAdapter::ActiveRecord
|
40
|
+
@receiving_model = Receiving::Model::ActiveRecord
|
59
41
|
when :sequel
|
60
|
-
ORMAdapter::Sequel
|
42
|
+
@publishing_adapter = Publishing::ORMAdapter::Sequel
|
43
|
+
@receiving_model = Receiving::Model::Sequel
|
61
44
|
else
|
62
|
-
raise
|
45
|
+
raise ORMNotSupported.new(val.inspect)
|
63
46
|
end
|
47
|
+
|
48
|
+
@orm = val
|
64
49
|
end
|
65
50
|
end
|
66
51
|
end
|
data/lib/table_sync/errors.rb
CHANGED
@@ -4,37 +4,23 @@ module TableSync
|
|
4
4
|
Error = Class.new(StandardError)
|
5
5
|
|
6
6
|
class UpsertError < Error
|
7
|
-
def initialize(data:, target_keys:,
|
8
|
-
super
|
9
|
-
Upsert has changed more than 1 row;
|
10
|
-
data: #{data.inspect}
|
11
|
-
target_keys: #{target_keys.inspect}
|
12
|
-
version_key: #{version_key.inspect}
|
13
|
-
first_sync_time_key: #{first_sync_time_key.inspect}
|
14
|
-
default_values: #{default_values.inspect}
|
15
|
-
MSG
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
class UndefinedConfig < Error
|
20
|
-
def initialize(model)
|
21
|
-
super("Config not defined for model; model: #{model.inspect}")
|
7
|
+
def initialize(data:, target_keys:, result:)
|
8
|
+
super("data: #{data.inspect}, target_keys: #{target_keys.inspect}, result: #{result.inspect}")
|
22
9
|
end
|
23
10
|
end
|
24
11
|
|
25
12
|
class DestroyError < Error
|
26
|
-
def initialize(data)
|
27
|
-
super("
|
13
|
+
def initialize(data:, target_keys:, result:)
|
14
|
+
super("data: #{data.inspect}, target_keys: #{target_keys.inspect}, result: #{result.inspect}")
|
28
15
|
end
|
29
16
|
end
|
30
17
|
|
31
|
-
class
|
32
|
-
|
33
|
-
# @param target_attributes [Hash<Symbol|String,Any>]
|
34
|
-
def initialize(target_keys, target_attributes)
|
18
|
+
class DataError < Error
|
19
|
+
def initialize(data, target_keys, description)
|
35
20
|
super(<<~MSG.squish)
|
36
|
-
|
37
|
-
|
21
|
+
#{description}
|
22
|
+
target_keys: #{target_keys}
|
23
|
+
data: #{data}
|
38
24
|
MSG
|
39
25
|
end
|
40
26
|
end
|
@@ -60,4 +46,34 @@ module TableSync
|
|
60
46
|
super("#{plugin_name} plugin already exists")
|
61
47
|
end
|
62
48
|
end
|
49
|
+
|
50
|
+
class InterfaceError < Error
|
51
|
+
def initialize(object, method_name, parameters, description)
|
52
|
+
parameters = parameters.map do |parameter|
|
53
|
+
type, name = parameter
|
54
|
+
|
55
|
+
case type
|
56
|
+
when :req
|
57
|
+
name.to_s
|
58
|
+
when :keyreq
|
59
|
+
"#{name}:"
|
60
|
+
when :block
|
61
|
+
"&#{name}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
signature = "#{method_name}(#{parameters.join(", ")})"
|
66
|
+
|
67
|
+
super("#{object} has to implement method `#{signature}`\n#{description}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
UndefinedEvent = Class.new(Error)
|
72
|
+
ORMNotSupported = Class.new(Error)
|
73
|
+
|
74
|
+
class WrongOptionValue < Error
|
75
|
+
def initialize(model, option, value)
|
76
|
+
super("TableSync config for #{model.inspect} can't contain #{value.inspect} as #{option}")
|
77
|
+
end
|
78
|
+
end
|
63
79
|
end
|