Skip to content

Commit b18c1b4

Browse files
authored
Add new option: on_first_sync (#98)
* Add new option. * Add new option. Skip rubocop warning. * Add new option. Spec. * Add new option. * Add new option. CHANGELOG. * Add new option. Review. * Add new option. Review. * Add new option. * Add new option. * Add new option. * Add new option. Review. * Add new option. Review. * Add new option.
1 parent 3a9c4ee commit b18c1b4

File tree

16 files changed

+371
-46
lines changed

16 files changed

+371
-46
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [6.10.0] - 2025-11-21
5+
### Fixed
6+
- Add on_first_sync callback
7+
48
## [6.9.3] - 2025-10-09
59
### Fixed
610
- Add validate_types to model interface

Gemfile.lock

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
table_sync (6.9.3)
4+
table_sync (6.10.0)
55
memery
66
rabbit_messaging (>= 1.7.0)
77
rails
@@ -90,12 +90,12 @@ GEM
9090
bunny (2.24.0)
9191
amq-protocol (~> 2.3)
9292
sorted_set (~> 1, >= 1.0.2)
93-
cgi (0.4.2)
93+
cgi (0.5.0)
9494
coderay (1.1.3)
9595
concurrent-ruby (1.3.5)
9696
connection_pool (2.5.3)
9797
crass (1.0.6)
98-
date (3.4.1)
98+
date (3.5.0)
9999
diff-lcs (1.6.2)
100100
docile (1.4.1)
101101
drb (2.2.3)
@@ -106,8 +106,8 @@ GEM
106106
activesupport (>= 6.1)
107107
i18n (1.14.7)
108108
concurrent-ruby (~> 1.0)
109-
io-console (0.8.0)
110-
irb (1.15.2)
109+
io-console (0.8.1)
110+
irb (1.15.3)
111111
pp (>= 0.6.0)
112112
rdoc (>= 4.0.0)
113113
reline (>= 0.4.2)
@@ -124,18 +124,19 @@ GEM
124124
loofah (2.24.1)
125125
crass (~> 1.0.2)
126126
nokogiri (>= 1.12.0)
127-
mail (2.8.1)
127+
mail (2.9.0)
128+
logger
128129
mini_mime (>= 0.1.1)
129130
net-imap
130131
net-pop
131132
net-smtp
132-
marcel (1.0.4)
133+
marcel (1.1.0)
133134
memery (1.7.0)
134135
method_source (1.1.0)
135136
mini_mime (1.1.5)
136137
mini_portile2 (2.8.9)
137138
minitest (5.25.5)
138-
net-imap (0.5.8)
139+
net-imap (0.5.12)
139140
date
140141
net-protocol
141142
net-pop (0.1.2)
@@ -170,7 +171,7 @@ GEM
170171
ast (~> 2.4.1)
171172
racc
172173
pg (1.5.9)
173-
pp (0.6.2)
174+
pp (0.6.3)
174175
prettyprint
175176
prettyprint (0.2.0)
176177
prism (1.4.0)
@@ -224,11 +225,12 @@ GEM
224225
rainbow (3.1.1)
225226
rake (13.2.1)
226227
rbtree (0.4.6)
227-
rdoc (6.14.0)
228+
rdoc (6.15.1)
228229
erb
229230
psych (>= 4.0.0)
231+
tsort
230232
regexp_parser (2.10.0)
231-
reline (0.6.1)
233+
reline (0.6.3)
232234
io-console (~> 0.5)
233235
rspec (3.13.1)
234236
rspec-core (~> 3.13.0)
@@ -308,10 +310,11 @@ GEM
308310
sorted_set (1.0.3)
309311
rbtree
310312
set (~> 1.0)
311-
stringio (3.1.7)
312-
thor (1.3.2)
313+
stringio (3.1.8)
314+
thor (1.4.0)
313315
timecop (0.9.10)
314316
timeout (0.4.3)
317+
tsort (0.2.0)
315318
tzinfo (2.0.6)
316319
concurrent-ruby (~> 1.0)
317320
unicode-display_width (3.1.4)

lib/table_sync/receiving.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Receiving
66
require_relative "receiving/config_decorator"
77
require_relative "receiving/dsl"
88
require_relative "receiving/handler"
9+
require_relative "receiving/hooks/once"
910
require_relative "receiving/model/active_record"
1011
require_relative "receiving/model/sequel"
1112
end

lib/table_sync/receiving/config.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative "hooks/once"
4+
35
module TableSync::Receiving
46
class Config
57
attr_reader :model, :events
@@ -23,13 +25,10 @@ def invalid_events
2325
class << self
2426
attr_reader :default_values_for_options
2527

26-
# In a configs this options are requested as they are
28+
# In a configs these options are requested as they are
2729
# config.option - get value
2830
# config.option(args) - set static value
2931
# config.option { ... } - set proc as value
30-
#
31-
# In `Receiving::Handler` or `Receiving::EventActions` this options are requested
32-
# through `Receiving::ConfigDecorator#method_missing` which always executes `config.option`
3332

3433
def add_option(name, value_setter_wrapper:, value_as_proc_setter_wrapper:, default:)
3534
ivar = :"@#{name}"
@@ -55,11 +54,30 @@ def add_option(name, value_setter_wrapper:, value_as_proc_setter_wrapper:, defau
5554
instance_variable_set(ivar, result_value)
5655
end
5756
end
57+
58+
def add_hook_option(name, hook_class:)
59+
ivar = :"@#{name}"
60+
61+
@default_values_for_options ||= {}
62+
@default_values_for_options[ivar] = proc { [] }
63+
64+
define_method(name) do |conditions, &handler|
65+
hooks = instance_variable_get(ivar)
66+
hooks ||= []
67+
68+
hooks << hook_class.new(conditions:, handler:)
69+
instance_variable_set(ivar, hooks)
70+
end
71+
end
5872
end
5973

6074
def allow_event?(name)
6175
events.include?(name)
6276
end
77+
78+
def option(name)
79+
instance_variable_get(:"@#{name}")
80+
end
6381
end
6482
end
6583

@@ -201,6 +219,11 @@ def allow_event?(name)
201219
value_as_proc_setter_wrapper: any_value,
202220
default: proc { proc { |&block| block.call } }
203221

222+
TableSync::Receiving::Config.add_hook_option(
223+
:on_first_sync,
224+
hook_class: TableSync::Receiving::Hooks::Once,
225+
)
226+
204227
%i[
205228
before_update
206229
after_commit_on_update

lib/table_sync/receiving/config_decorator.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
module TableSync::Receiving
44
class ConfigDecorator
5-
extend Forwardable
6-
7-
def_delegators :@config, :allow_event?
85
# rubocop:disable Metrics/ParameterLists
96
def initialize(config:, event:, model:, version:, project_id:, raw_data:)
107
@config = config
@@ -19,9 +16,17 @@ def initialize(config:, event:, model:, version:, project_id:, raw_data:)
1916
end
2017
# rubocop:enable Metrics/ParameterLists
2118

22-
def method_missing(name, **additional_params, &)
23-
value = @config.send(name)
19+
def option(name, **additional_params, &)
20+
value = @config.option(name)
2421
value.is_a?(Proc) ? value.call(@default_params.merge(additional_params), &) : value
2522
end
23+
24+
def model
25+
@config.model
26+
end
27+
28+
def allow_event?(name)
29+
@config.allow_event?(name)
30+
end
2631
end
2732
end

lib/table_sync/receiving/handler.rb

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,20 @@ def call
2222

2323
next if data.empty?
2424

25-
version_key = config.version_key(data:)
26-
data.each { |row| row[version_key] = version }
27-
28-
target_keys = config.target_keys(data:)
25+
target_keys = config.option(:target_keys, data:)
2926

3027
validate_data(data, target_keys:)
3128

3229
data.sort_by! { |row| row.values_at(*target_keys).map { |value| sort_key(value) } }
3330

31+
version_key = config.option(:version_key, data:)
3432
params = { data:, target_keys:, version_key: }
3533

3634
if event == :update
37-
params[:default_values] = config.default_values(data:)
35+
params[:default_values] = config.option(:default_values, data:)
3836
end
3937

40-
config.wrap_receiving(event:, **params) do
38+
config.option(:wrap_receiving, **params) do
4139
perform(config, params)
4240
end
4341
end
@@ -83,25 +81,28 @@ def configs
8381
end
8482

8583
def processed_data(config)
84+
version_key = config.option(:version_key, data:)
8685
data.filter_map do |row|
87-
next if config.skip(row:)
86+
next if config.option(:skip, row:)
8887

8988
row = row.dup
9089

91-
config.mapping_overrides(row:).each do |before, after|
90+
config.option(:mapping_overrides, row:).each do |before, after|
9291
row[after] = row.delete(before)
9392
end
9493

95-
config.except(row:).each { |x| row.delete(x) }
94+
config.option(:except, row:).each { |x| row.delete(x) }
9695

97-
row.merge!(config.additional_data(row:))
96+
row.merge!(config.option(:additional_data, row:))
9897

99-
only = config.only(row:)
98+
only = config.option(:only, row:)
10099
row, rest = row.partition { |key, _| key.in?(only) }.map(&:to_h)
101100

102-
rest_key = config.rest_key(row:, rest:)
101+
rest_key = config.option(:rest_key, row:, rest:)
103102
(row[rest_key] ||= {}).merge!(rest) if rest_key
104103

104+
row[version_key] = version
105+
105106
row
106107
end
107108
end
@@ -138,16 +139,16 @@ def validate_data_types(model, data)
138139
raise TableSync::DataError.new(data, errors.keys, errors.to_json)
139140
end
140141

141-
def perform(config, params)
142+
def perform(config, params) # rubocop:disable Metrics/MethodLength
142143
model = config.model
143144

144145
model.transaction do
145146
results = if event == :update
146-
config.before_update(**params)
147+
config.option(:before_update, **params)
147148
validate_data_types(model, params[:data])
148149
model.upsert(**params)
149150
else
150-
config.before_destroy(**params)
151+
config.option(:before_destroy, **params)
151152
model.destroy(**params)
152153
end
153154

@@ -157,9 +158,15 @@ def perform(config, params)
157158
end
158159

159160
if event == :update
160-
model.after_commit { config.after_commit_on_update(**params, results:) }
161+
model.after_commit do
162+
config.option(:after_commit_on_update, **params, results:)
163+
164+
Array(config.option(:on_first_sync)).each do |hook|
165+
hook.perform(config:, targets: results) if hook.enabled?
166+
end
167+
end
161168
else
162-
model.after_commit { config.after_commit_on_destroy(**params, results:) }
169+
model.after_commit { config.option(:after_commit_on_destroy, **params, results:) }
163170
end
164171
end
165172
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module TableSync::Receiving::Hooks
4+
class Once
5+
LOCK_KEY = "hook-once-lock-key"
6+
7+
attr_reader :conditions, :handler, :lookup_code
8+
9+
def initialize(conditions:, handler:)
10+
@conditions = conditions
11+
@handler = handler
12+
init_lookup_code
13+
end
14+
15+
def enabled?
16+
conditions[:columns].any?
17+
end
18+
19+
def perform(config:, targets:)
20+
target_keys = config.option(:target_keys)
21+
model = config.model
22+
23+
targets.each do |target|
24+
next unless conditions?(target)
25+
26+
keys = target.slice(*target_keys)
27+
model.try_advisory_lock(prepare_lock_key(keys)) do
28+
model.find_and_save(keys:) do |entry|
29+
next unless allow?(entry)
30+
31+
entry.hooks ||= []
32+
entry.hooks << lookup_code
33+
model.after_commit { handler.call(entry:) }
34+
end
35+
end
36+
end
37+
end
38+
39+
private
40+
41+
def allow?(entry)
42+
Array(entry.hooks).exclude?(lookup_code)
43+
end
44+
45+
def init_lookup_code
46+
@lookup_code = conditions[:columns].map do |column|
47+
"#{column}-#{conditions[column]}"
48+
end.join(":")
49+
end
50+
51+
def conditions?(row)
52+
conditions[:columns].all? do |column|
53+
row[column] == (conditions[column] || row[column])
54+
end
55+
end
56+
57+
def prepare_lock_key(row_keys)
58+
lock_keys = [LOCK_KEY] + row_keys.values
59+
Zlib.crc32(lock_keys.join(":")) % (2**31)
60+
end
61+
end
62+
end

0 commit comments

Comments
 (0)