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
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync
|
4
|
+
module DSL
|
5
|
+
def inherited(klass)
|
6
|
+
klass.instance_variable_set(:@configs, configs.deep_dup)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def configs
|
11
|
+
@configs ||= Hash.new { |hash, key| hash[key] = [] }
|
12
|
+
end
|
13
|
+
|
14
|
+
def receive(source, to_table:, events: nil, &block)
|
15
|
+
config = ::TableSync::Config.new(
|
16
|
+
model: TableSync.orm.model.new(to_table),
|
17
|
+
events: events,
|
18
|
+
)
|
19
|
+
|
20
|
+
config.instance_exec(&block) if block
|
21
|
+
|
22
|
+
configs[source.to_s] << config
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
|
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 DestroyError < Error
|
20
|
+
def initialize(data)
|
21
|
+
super("Destroy has changed more than 1 row; data: #{data.inspect}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class UndefinedConfig < Error
|
26
|
+
def initialize(model)
|
27
|
+
super("Config not defined for model; model: #{model.inspect}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync
|
4
|
+
module EventActions
|
5
|
+
def update(data) # rubocop:disable Metrics/MethodLength
|
6
|
+
model.transaction do
|
7
|
+
args = {
|
8
|
+
data: data,
|
9
|
+
target_keys: target_keys,
|
10
|
+
version_key: version_key,
|
11
|
+
first_sync_time_key: first_sync_time_key,
|
12
|
+
default_values: default_values,
|
13
|
+
}
|
14
|
+
|
15
|
+
@config.callback_registry.get_callbacks(kind: :before_commit, event: :update).each do |cb|
|
16
|
+
cb[data.values.flatten]
|
17
|
+
end
|
18
|
+
|
19
|
+
results = data.reduce([]) do |upserts, (part_model, part_data)|
|
20
|
+
upserts + part_model.upsert(**args, data: part_data)
|
21
|
+
end
|
22
|
+
|
23
|
+
return if results.empty?
|
24
|
+
raise TableSync::UpsertError.new(**args) unless correct_keys?(results)
|
25
|
+
|
26
|
+
@config.model.after_commit do
|
27
|
+
@config.callback_registry.get_callbacks(kind: :after_commit, event: :update).each do |cb|
|
28
|
+
cb[results]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy(data)
|
35
|
+
attributes = data.first || {}
|
36
|
+
target_attributes = attributes.select { |key| target_keys.include?(key) }
|
37
|
+
|
38
|
+
model.transaction do
|
39
|
+
@config.callback_registry.get_callbacks(kind: :before_commit, event: :destroy).each do |cb|
|
40
|
+
cb[attributes]
|
41
|
+
end
|
42
|
+
|
43
|
+
results = model.destroy(target_attributes)
|
44
|
+
|
45
|
+
return if results.empty?
|
46
|
+
raise TableSync::DestroyError.new(target_attributes) if results.size != 1
|
47
|
+
|
48
|
+
@config.model.after_commit do
|
49
|
+
@config.callback_registry.get_callbacks(kind: :after_commit, event: :destroy).each do |cb|
|
50
|
+
cb[results]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def correct_keys?(query_results)
|
57
|
+
query_results.uniq { |d| d.slice(*target_keys) }.size == query_results.size
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync::Model
|
4
|
+
class ActiveRecord
|
5
|
+
class AfterCommitWrap
|
6
|
+
def initialize(&block)
|
7
|
+
@callback = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def committed!(*)
|
11
|
+
@callback.call
|
12
|
+
end
|
13
|
+
|
14
|
+
def before_committed!(*); end
|
15
|
+
|
16
|
+
def rolledback!(*); end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(table_name)
|
20
|
+
@raw_model = Class.new(::ActiveRecord::Base) do
|
21
|
+
self.table_name = table_name
|
22
|
+
self.inheritance_column = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def columns
|
27
|
+
raw_model.column_names.map(&:to_sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def primary_keys
|
31
|
+
db.execute(<<~SQL).column_values(0).map(&:to_sym)
|
32
|
+
SELECT kcu.column_name
|
33
|
+
FROM INFORMATION_SCHEMA.TABLES t
|
34
|
+
LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
35
|
+
ON tc.table_catalog = t.table_catalog
|
36
|
+
AND tc.table_schema = t.table_schema
|
37
|
+
AND tc.table_name = t.table_name
|
38
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
39
|
+
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
40
|
+
ON kcu.table_catalog = tc.table_catalog
|
41
|
+
AND kcu.table_schema = tc.table_schema
|
42
|
+
AND kcu.table_name = tc.table_name
|
43
|
+
AND kcu.constraint_name = tc.constraint_name
|
44
|
+
WHERE
|
45
|
+
t.table_schema NOT IN ('pg_catalog', 'information_schema')
|
46
|
+
AND t.table_schema = '#{table_info[:schema]}'
|
47
|
+
AND t.table_name = '#{table_info[:name]}'
|
48
|
+
ORDER BY
|
49
|
+
kcu.ordinal_position
|
50
|
+
SQL
|
51
|
+
end
|
52
|
+
|
53
|
+
def upsert(data:, target_keys:, version_key:, first_sync_time_key:, default_values:)
|
54
|
+
data = Array.wrap(data)
|
55
|
+
|
56
|
+
transaction do
|
57
|
+
data.map do |datum|
|
58
|
+
conditions = datum.select { |k| target_keys.include?(k) }
|
59
|
+
|
60
|
+
row = raw_model.lock("FOR NO KEY UPDATE").find_by(conditions)
|
61
|
+
if row
|
62
|
+
next if datum[version_key] <= row[version_key]
|
63
|
+
|
64
|
+
row.update!(datum)
|
65
|
+
else
|
66
|
+
create_data = datum.merge(default_values)
|
67
|
+
create_data[first_sync_time_key] = Time.current if first_sync_time_key
|
68
|
+
row = raw_model.create!(create_data)
|
69
|
+
end
|
70
|
+
|
71
|
+
row_to_hash(row)
|
72
|
+
end.compact
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def destroy(data)
|
77
|
+
transaction do
|
78
|
+
row = raw_model.lock("FOR UPDATE").find_by(data)&.destroy!
|
79
|
+
[row_to_hash(row)]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def transaction(&block)
|
84
|
+
::ActiveRecord::Base.transaction(&block)
|
85
|
+
end
|
86
|
+
|
87
|
+
def after_commit(&block)
|
88
|
+
db.add_transaction_record(AfterCommitWrap.new(&block))
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
attr_reader :raw_model
|
94
|
+
|
95
|
+
def table_info
|
96
|
+
keys = raw_model.table_name.split(".")
|
97
|
+
name = keys[-1]
|
98
|
+
schema = keys[-2] || "public"
|
99
|
+
{ schema: schema, name: name }
|
100
|
+
end
|
101
|
+
|
102
|
+
def db
|
103
|
+
@raw_model.connection
|
104
|
+
end
|
105
|
+
|
106
|
+
def row_to_hash(row)
|
107
|
+
row.attributes.each_with_object({}) { |(k, v), o| o[k.to_sym] = v }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync::Model
|
4
|
+
class Sequel
|
5
|
+
def initialize(table_name)
|
6
|
+
@raw_model = Class.new(::Sequel::Model(table_name)).tap(&:unrestrict_primary_key)
|
7
|
+
end
|
8
|
+
|
9
|
+
def columns
|
10
|
+
dataset.columns
|
11
|
+
end
|
12
|
+
|
13
|
+
def primary_keys
|
14
|
+
[raw_model.primary_key].flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
def upsert(data:, target_keys:, version_key:, first_sync_time_key:, default_values:)
|
18
|
+
data = Array.wrap(data)
|
19
|
+
qualified_version = ::Sequel.qualify(table_name, version_key)
|
20
|
+
version_condition = ::Sequel.function(:coalesce, qualified_version, 0) <
|
21
|
+
::Sequel.qualify(:excluded, version_key)
|
22
|
+
|
23
|
+
upd_spec = update_spec(data.first.keys - target_keys)
|
24
|
+
data.map! { |d| d.merge(default_values) }
|
25
|
+
|
26
|
+
insert_data = type_cast(data)
|
27
|
+
if first_sync_time_key
|
28
|
+
insert_data.each { |datum| datum[first_sync_time_key] = Time.current }
|
29
|
+
end
|
30
|
+
|
31
|
+
dataset.returning
|
32
|
+
.insert_conflict(
|
33
|
+
target: target_keys,
|
34
|
+
update: upd_spec,
|
35
|
+
update_where: version_condition,
|
36
|
+
)
|
37
|
+
.multi_insert(insert_data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def destroy(data)
|
41
|
+
dataset.returning.where(data).delete
|
42
|
+
end
|
43
|
+
|
44
|
+
def transaction(&block)
|
45
|
+
db.transaction(&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def after_commit(&block)
|
49
|
+
db.after_commit(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :raw_model
|
55
|
+
|
56
|
+
def table_name
|
57
|
+
raw_model.table_name
|
58
|
+
end
|
59
|
+
|
60
|
+
def dataset
|
61
|
+
raw_model.dataset
|
62
|
+
end
|
63
|
+
|
64
|
+
def db
|
65
|
+
dataset.db
|
66
|
+
end
|
67
|
+
|
68
|
+
def type_cast(data)
|
69
|
+
data.map { |d| raw_model.new(d).values.keep_if { |k| d.key?(k) } }
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_spec(keys)
|
73
|
+
keys.map { |key| [key, ::Sequel[:excluded][key]] }.to_h
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync::ORMAdapter
|
4
|
+
module ActiveRecord
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def model
|
8
|
+
::TableSync::Model::ActiveRecord
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(dataset, conditions)
|
12
|
+
dataset.find_by(conditions)
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes(object)
|
16
|
+
object.attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_sync(klass, **opts)
|
20
|
+
klass.instance_exec do
|
21
|
+
{ create: :created, update: :updated, destroy: :destroyed }.each do |event, state|
|
22
|
+
after_commit(on: event, **opts) do
|
23
|
+
TableSync::Publisher.new(self.class.name, attributes, state: state).publish
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TableSync::ORMAdapter
|
4
|
+
module Sequel
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def model
|
8
|
+
::TableSync::Model::Sequel
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(dataset, conditions)
|
12
|
+
dataset.find(conditions)
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes(object)
|
16
|
+
object.values
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_sync(klass, **opts)
|
20
|
+
if_predicate = to_predicate(opts.delete(:if), true)
|
21
|
+
unless_predicate = to_predicate(opts.delete(:unless), false)
|
22
|
+
raise "Only :if and :unless options are currently supported for Sequel hooks" if opts.any?
|
23
|
+
|
24
|
+
{ create: :created, update: :updated }.each do |event, state|
|
25
|
+
klass.send(:define_method, :"after_#{event}") do
|
26
|
+
if instance_eval(&if_predicate) && !instance_eval(&unless_predicate)
|
27
|
+
db.after_commit do
|
28
|
+
TableSync::Publisher.new(self.class.name, values, state: state).publish
|
29
|
+
end
|
30
|
+
end
|
31
|
+
super()
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
klass.send(:define_method, :after_destroy) do
|
36
|
+
# publish anyway
|
37
|
+
db.after_commit do
|
38
|
+
TableSync::Publisher.new(self.class.name, values, state: :destroyed).publish
|
39
|
+
end
|
40
|
+
super()
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_predicate(val, default)
|
45
|
+
return val.to_proc if val.respond_to?(:to_proc)
|
46
|
+
|
47
|
+
-> (*) { default }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TableSync::Publisher < TableSync::BasePublisher
|
4
|
+
DEBOUNCE_TIME = 1.minute
|
5
|
+
|
6
|
+
# 'original_attributes' are not published, they are used to resolve the routing key
|
7
|
+
def initialize(object_class, original_attributes, destroyed: nil, confirm: true, state: :updated)
|
8
|
+
@object_class = object_class.constantize
|
9
|
+
@original_attributes = filter_safe_for_serialization(original_attributes.deep_symbolize_keys)
|
10
|
+
@confirm = confirm
|
11
|
+
|
12
|
+
if destroyed.nil?
|
13
|
+
@state = validate_state(state)
|
14
|
+
else
|
15
|
+
# TODO Legacy job support, remove
|
16
|
+
@state = destroyed ? :destroyed : :updated
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def publish
|
21
|
+
return enqueue_job if destroyed?
|
22
|
+
|
23
|
+
sync_time = Rails.cache.read(cache_key) || current_time - DEBOUNCE_TIME - 1.second
|
24
|
+
return if sync_time > current_time
|
25
|
+
|
26
|
+
next_sync_time = sync_time + DEBOUNCE_TIME
|
27
|
+
next_sync_time <= current_time ? enqueue_job : enqueue_job(next_sync_time)
|
28
|
+
end
|
29
|
+
|
30
|
+
def publish_now
|
31
|
+
# Update request and object does not exist -> skip publishing
|
32
|
+
return if !object && !destroyed?
|
33
|
+
|
34
|
+
Rabbit.publish(params)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :original_attributes
|
40
|
+
attr_reader :state
|
41
|
+
|
42
|
+
def attrs_for_callables
|
43
|
+
original_attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
def attrs_for_routing_key
|
47
|
+
return object.attrs_for_routing_key if attrs_for_routing_key_defined?
|
48
|
+
attrs_for_callables
|
49
|
+
end
|
50
|
+
|
51
|
+
def attrs_for_metadata
|
52
|
+
return object.attrs_for_metadata if attrs_for_metadata_defined?
|
53
|
+
attrs_for_callables
|
54
|
+
end
|
55
|
+
|
56
|
+
def job_callable
|
57
|
+
TableSync.publishing_job_class_callable
|
58
|
+
end
|
59
|
+
|
60
|
+
def job_callable_error_message
|
61
|
+
"Can't publish, set TableSync.publishing_job_class_callable"
|
62
|
+
end
|
63
|
+
|
64
|
+
def enqueue_job(perform_at = current_time)
|
65
|
+
job = job_class.set(wait_until: perform_at)
|
66
|
+
job.perform_later(object_class.name, original_attributes, state: state.to_s, confirm: confirm?)
|
67
|
+
Rails.cache.write(cache_key, perform_at)
|
68
|
+
end
|
69
|
+
|
70
|
+
def routing_key
|
71
|
+
resolve_routing_key
|
72
|
+
end
|
73
|
+
|
74
|
+
def publishing_data
|
75
|
+
{
|
76
|
+
**super,
|
77
|
+
event: (destroyed? ? :destroy : :update),
|
78
|
+
metadata: { created: created? },
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def attributes_for_sync
|
83
|
+
if destroyed?
|
84
|
+
if object_class.respond_to?(:table_sync_destroy_attributes)
|
85
|
+
object_class.table_sync_destroy_attributes(original_attributes)
|
86
|
+
else
|
87
|
+
needle
|
88
|
+
end
|
89
|
+
elsif attributes_for_sync_defined?
|
90
|
+
object.attributes_for_sync
|
91
|
+
else
|
92
|
+
TableSync.orm.attributes(object)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
memoize def object
|
97
|
+
TableSync.orm.find(object_class, needle)
|
98
|
+
end
|
99
|
+
|
100
|
+
def needle
|
101
|
+
original_attributes.slice(*primary_keys)
|
102
|
+
end
|
103
|
+
|
104
|
+
def cache_key
|
105
|
+
"#{object_class}/#{needle}_table_sync_time".delete(" ")
|
106
|
+
end
|
107
|
+
|
108
|
+
def destroyed?
|
109
|
+
state == :destroyed
|
110
|
+
end
|
111
|
+
|
112
|
+
def created?
|
113
|
+
state == :created
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_state(state)
|
117
|
+
if %i[created updated destroyed].include?(state&.to_sym)
|
118
|
+
state.to_sym
|
119
|
+
else
|
120
|
+
raise "Unknown state: #{state.inspect}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TableSync::ReceivingHandler < Rabbit::EventHandler
|
4
|
+
extend TableSync::DSL
|
5
|
+
|
6
|
+
attribute :event
|
7
|
+
attribute :model
|
8
|
+
attribute :version
|
9
|
+
|
10
|
+
def call
|
11
|
+
raise TableSync::UndefinedConfig.new(model) if configs.blank?
|
12
|
+
|
13
|
+
configs.each do |config|
|
14
|
+
next unless config.allow_event?(event)
|
15
|
+
|
16
|
+
data = processed_data(config)
|
17
|
+
next if data.empty?
|
18
|
+
|
19
|
+
case event
|
20
|
+
when :update
|
21
|
+
config.model.transaction do
|
22
|
+
config.update(data)
|
23
|
+
end
|
24
|
+
when :destroy
|
25
|
+
config.destroy(data.values.first)
|
26
|
+
else
|
27
|
+
raise "Unknown event: #{event}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def data=(data)
|
35
|
+
@data = data[:attributes]
|
36
|
+
end
|
37
|
+
|
38
|
+
def event=(name)
|
39
|
+
super(name.to_sym)
|
40
|
+
end
|
41
|
+
|
42
|
+
def model=(name)
|
43
|
+
super(name.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
def configs
|
47
|
+
@configs ||= self.class.configs[model]
|
48
|
+
&.map { |c| ::TableSync::ConfigDecorator.new(c, self) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def processed_data(config)
|
52
|
+
parts = config.partitions&.transform_keys { |k| config.model.class.new(k) } ||
|
53
|
+
{ config.model => Array.wrap(data) }
|
54
|
+
|
55
|
+
parts.transform_values! do |data_part|
|
56
|
+
data_part.map do |row|
|
57
|
+
original_row_for_data = row.dup
|
58
|
+
row = row.dup
|
59
|
+
|
60
|
+
config.mapping_overrides.each do |before, after|
|
61
|
+
row[after] = row.delete(before)
|
62
|
+
end
|
63
|
+
|
64
|
+
only = config.only
|
65
|
+
row, missed = row.partition { |key, _| key.in?(only) }.map(&:to_h)
|
66
|
+
|
67
|
+
row.deep_merge!(config.rest_key => missed) if config.rest_key
|
68
|
+
row[config.version_key] = version
|
69
|
+
|
70
|
+
row.merge!(config.additional_data(original_row_for_data))
|
71
|
+
|
72
|
+
row unless config.skip(original_row_for_data)
|
73
|
+
end.compact.presence
|
74
|
+
end.compact
|
75
|
+
end
|
76
|
+
end
|
data/lib/table_sync/version.rb
CHANGED
data/lib/table_sync.rb
CHANGED
@@ -1,5 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "memery"
|
4
|
+
require "rabbit_messaging"
|
5
|
+
require "rabbit/event_handler" # NOTE: from rabbit_messaging"
|
6
|
+
require "active_support/core_ext/object/blank"
|
7
|
+
require "active_support/core_ext/numeric/time"
|
8
|
+
|
3
9
|
module TableSync
|
4
|
-
|
10
|
+
require_relative "./table_sync/version"
|
11
|
+
require_relative "./table_sync/errors"
|
12
|
+
require_relative "./table_sync/event_actions"
|
13
|
+
require_relative "./table_sync/config"
|
14
|
+
require_relative "./table_sync/config/callback_registry"
|
15
|
+
require_relative "./table_sync/config_decorator"
|
16
|
+
require_relative "./table_sync/dsl"
|
17
|
+
require_relative "./table_sync/receiving_handler"
|
18
|
+
require_relative "./table_sync/base_publisher"
|
19
|
+
require_relative "./table_sync/publisher"
|
20
|
+
require_relative "./table_sync/batch_publisher"
|
21
|
+
require_relative "./table_sync/orm_adapter/active_record"
|
22
|
+
require_relative "./table_sync/orm_adapter/sequel"
|
23
|
+
require_relative "./table_sync/model/active_record"
|
24
|
+
require_relative "./table_sync/model/sequel"
|
25
|
+
|
26
|
+
class << self
|
27
|
+
include Memery
|
28
|
+
|
29
|
+
attr_accessor :publishing_job_class_callable
|
30
|
+
attr_accessor :batch_publishing_job_class_callable
|
31
|
+
attr_accessor :routing_key_callable
|
32
|
+
attr_accessor :exchange_name
|
33
|
+
attr_accessor :routing_metadata_callable
|
34
|
+
|
35
|
+
def sync(*args)
|
36
|
+
orm.setup_sync(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def orm=(val)
|
40
|
+
clear_memery_cache!
|
41
|
+
@orm = val
|
42
|
+
end
|
43
|
+
|
44
|
+
memoize def orm
|
45
|
+
case @orm
|
46
|
+
when :active_record
|
47
|
+
ORMAdapter::ActiveRecord
|
48
|
+
when :sequel
|
49
|
+
ORMAdapter::Sequel
|
50
|
+
else
|
51
|
+
raise "ORM not supported: #{@orm.inspect}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
5
55
|
end
|
data/table_sync.gemspec
CHANGED
@@ -11,8 +11,10 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.version = TableSync::VERSION
|
12
12
|
spec.authors = ["Umbrellio"]
|
13
13
|
spec.email = ["oss@umbrellio.biz"]
|
14
|
-
spec.summary = "
|
15
|
-
|
14
|
+
spec.summary = "DB Table synchronization between microservices " \
|
15
|
+
"based on Model's event system and RabbitMQ messaging"
|
16
|
+
spec.description = "DB Table synchronization between microservices " \
|
17
|
+
"based on Model's event system and RabbitMQ messaging"
|
16
18
|
spec.homepage = "https://github.com/umbrellio/table_sync"
|
17
19
|
spec.license = "MIT"
|
18
20
|
|
@@ -24,12 +26,23 @@ Gem::Specification.new do |spec|
|
|
24
26
|
f.match(%r{^(test|spec|features)/})
|
25
27
|
end
|
26
28
|
|
29
|
+
spec.add_runtime_dependency "memery"
|
30
|
+
spec.add_runtime_dependency "rabbit_messaging", "~> 0.3"
|
31
|
+
spec.add_runtime_dependency "rails"
|
32
|
+
|
27
33
|
spec.add_development_dependency "coveralls", "~> 0.8"
|
28
34
|
spec.add_development_dependency "rspec", "~> 3.8"
|
29
35
|
spec.add_development_dependency "rubocop-config-umbrellio", "~> 0.70"
|
30
36
|
spec.add_development_dependency "simplecov", "~> 0.16"
|
31
37
|
|
38
|
+
spec.add_development_dependency "activejob"
|
39
|
+
spec.add_development_dependency "activerecord"
|
40
|
+
spec.add_development_dependency "pg", "~> 0.18"
|
41
|
+
spec.add_development_dependency "sequel"
|
42
|
+
spec.add_development_dependency "timecop"
|
43
|
+
|
32
44
|
spec.add_development_dependency "bundler"
|
45
|
+
spec.add_development_dependency "bundler-audit"
|
33
46
|
spec.add_development_dependency "pry"
|
34
47
|
spec.add_development_dependency "rake"
|
35
48
|
end
|