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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/CHANGELOG.md +40 -0
  4. data/Gemfile.lock +82 -77
  5. data/README.md +4 -2
  6. data/docs/message_protocol.md +24 -0
  7. data/docs/notifications.md +45 -0
  8. data/docs/publishing.md +147 -0
  9. data/docs/receiving.md +341 -0
  10. data/lib/table_sync.rb +16 -31
  11. data/lib/table_sync/errors.rb +39 -23
  12. data/lib/table_sync/publishing.rb +11 -0
  13. data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
  14. data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +4 -4
  15. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +3 -7
  16. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +2 -6
  17. data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
  18. data/lib/table_sync/receiving.rb +14 -0
  19. data/lib/table_sync/receiving/config.rb +218 -0
  20. data/lib/table_sync/receiving/config_decorator.rb +27 -0
  21. data/lib/table_sync/receiving/dsl.rb +28 -0
  22. data/lib/table_sync/receiving/handler.rb +131 -0
  23. data/lib/table_sync/{model → receiving/model}/active_record.rb +36 -22
  24. data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
  25. data/lib/table_sync/utils.rb +9 -0
  26. data/lib/table_sync/utils/interface_checker.rb +97 -0
  27. data/lib/table_sync/utils/proc_array.rb +17 -0
  28. data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
  29. data/lib/table_sync/version.rb +1 -1
  30. data/table_sync.gemspec +2 -1
  31. metadata +42 -30
  32. data/docs/development.md +0 -43
  33. data/docs/synopsis.md +0 -336
  34. data/lib/table_sync/config.rb +0 -105
  35. data/lib/table_sync/config/callback_registry.rb +0 -53
  36. data/lib/table_sync/config_decorator.rb +0 -38
  37. data/lib/table_sync/dsl.rb +0 -25
  38. data/lib/table_sync/event_actions.rb +0 -96
  39. data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
  40. data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
  41. data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
  42. data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
  43. data/lib/table_sync/plugins.rb +0 -72
  44. data/lib/table_sync/plugins/abstract.rb +0 -55
  45. data/lib/table_sync/plugins/access_mixin.rb +0 -49
  46. data/lib/table_sync/plugins/registry.rb +0 -153
  47. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -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
+ ```
@@ -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
- # @api public
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
- orm.setup_sync(klass, opts)
33
+ publishing_adapter.setup_sync(klass, opts)
48
34
  end
49
35
 
50
36
  def orm=(val)
51
- clear_memery_cache!
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 "ORM not supported: #{@orm.inspect}"
45
+ raise ORMNotSupported.new(val.inspect)
63
46
  end
47
+
48
+ @orm = val
64
49
  end
65
50
  end
66
51
  end
@@ -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:, version_key:, first_sync_time_key:, default_values:)
8
- super <<~MSG
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("Destroy has changed more than 1 row; data: #{data.inspect}")
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 UnprovidedEventTargetKeysError < Error
32
- # @param target_keys [Array<Symbol,String>]
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
- Some target keys not found in received attributes!
37
- (Expects: #{target_keys}, Actual: #{target_attributes.keys})
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