harmonia 0.2.3 → 0.2.5
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/README.md +9 -9
- data/lib/generators/harmonia/install_generator.rb +21 -5
- data/lib/generators/harmonia/reverse_sync_generator.rb +5 -2
- data/lib/generators/harmonia/sync_generator.rb +7 -2
- data/lib/generators/harmonia/templates/activerecord_to_filemaker_syncer_template.rb +85 -84
- data/lib/generators/harmonia/templates/add_server_id_to_harmonia_syncs.rb +10 -0
- data/lib/generators/harmonia/templates/concerns/photo_syncable.rb +28 -0
- data/lib/generators/harmonia/templates/concerns/related_model_resolver.rb +20 -0
- data/lib/generators/harmonia/templates/concerns/related_model_validation.rb +16 -0
- data/lib/generators/harmonia/templates/concerns/sync_loggable.rb +50 -0
- data/lib/generators/harmonia/templates/concerns/sync_retryable.rb +113 -0
- data/lib/generators/harmonia/templates/create_harmonia_syncs.rb +2 -0
- data/lib/generators/harmonia/templates/filemaker_to_activerecord_syncer_template.rb +79 -79
- data/lib/generators/harmonia/templates/harmonia_sync.rb +7 -16
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba24e3fb10ef3510c855bda693d015d4a0b158f0f076af681389d6a26dac6c9e
|
|
4
|
+
data.tar.gz: 69b3c0a459c92772cc179e6e5584ab38762e6e9dab1898d1787b27aaa9640634
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f940e8b3bca9c1ce1cca522051de2af62a0c9b82b4bab54e0001cef86bab7638086408f66bc6e1eaf76d90f090403871310c750070d534668a187fbadb0f454
|
|
7
|
+
data.tar.gz: faeb5432593d35938dac873e4aca8240cabcbac7ceb5f902f9756d121f34d00d9abf200f04e66a5d3232a9c1d1f42038c86935e0d07b32103871238afd56f55d
|
data/README.md
CHANGED
|
@@ -169,14 +169,14 @@ class ProductSyncer
|
|
|
169
169
|
@total_create_required = filemaker_records.length
|
|
170
170
|
|
|
171
171
|
existing_ids = Product.pluck(:filemaker_id)
|
|
172
|
-
filemaker_records.reject { |record| existing_ids.include?(record.
|
|
172
|
+
filemaker_records.reject { |record| existing_ids.include?(record.id) }
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
def records_to_update
|
|
176
176
|
filemaker_records = Trophonius::Product.all
|
|
177
177
|
|
|
178
178
|
records_needing_update = filemaker_records.select { |fm_record|
|
|
179
|
-
pg_record = Product.find_by(filemaker_id: fm_record.
|
|
179
|
+
pg_record = Product.find_by(filemaker_id: fm_record.id)
|
|
180
180
|
pg_record && needs_update?(fm_record, pg_record)
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -185,7 +185,7 @@ class ProductSyncer
|
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def records_to_delete
|
|
188
|
-
filemaker_ids = Trophonius::Product.all.map(&:
|
|
188
|
+
filemaker_ids = Trophonius::Product.all.map(&:id)
|
|
189
189
|
Product.where.not(filemaker_id: filemaker_ids).pluck(:id)
|
|
190
190
|
end
|
|
191
191
|
|
|
@@ -393,7 +393,7 @@ module Trophonius
|
|
|
393
393
|
# Converts FileMaker record to PostgreSQL attributes
|
|
394
394
|
def self.to_pg(record)
|
|
395
395
|
{
|
|
396
|
-
filemaker_id: record.
|
|
396
|
+
filemaker_id: record.id,
|
|
397
397
|
name: record.field_data['ProductName'],
|
|
398
398
|
price: record.field_data['Price'].to_f,
|
|
399
399
|
# ... map other fields
|
|
@@ -432,8 +432,8 @@ add_index :products, :filemaker_id, unique: true
|
|
|
432
432
|
|
|
433
433
|
This column serves as the bridge between your ActiveRecord records and FileMaker records:
|
|
434
434
|
|
|
435
|
-
- **FileMaker to ActiveRecord**: Stores the FileMaker `
|
|
436
|
-
- **ActiveRecord to FileMaker**: Can store the FileMaker `
|
|
435
|
+
- **FileMaker to ActiveRecord**: Stores the FileMaker `id` to identify which FileMaker record corresponds to each Rails record
|
|
436
|
+
- **ActiveRecord to FileMaker**: Can store the FileMaker `id` after creation, or you can use a separate field in FileMaker (like `PostgreSQLID`) to maintain the relationship
|
|
437
437
|
|
|
438
438
|
**Important Notes**:
|
|
439
439
|
- The column is indexed with a unique constraint for performance and data integrity
|
|
@@ -601,15 +601,15 @@ RSpec.describe ProductSyncer do
|
|
|
601
601
|
it 'returns records not in PostgreSQL' do
|
|
602
602
|
# Mock FileMaker records
|
|
603
603
|
allow(Trophonius::Product).to receive(:all).and_return([
|
|
604
|
-
double(
|
|
605
|
-
double(
|
|
604
|
+
double(id: 1),
|
|
605
|
+
double(id: 2)
|
|
606
606
|
])
|
|
607
607
|
|
|
608
608
|
# Mock existing PostgreSQL records
|
|
609
609
|
allow(Product).to receive(:pluck).with(:filemaker_id).and_return([1])
|
|
610
610
|
|
|
611
611
|
result = syncer.send(:records_to_create)
|
|
612
|
-
expect(result.map(&:
|
|
612
|
+
expect(result.map(&:id)).to eq([2])
|
|
613
613
|
end
|
|
614
614
|
end
|
|
615
615
|
end
|
|
@@ -23,10 +23,22 @@ module Harmonia
|
|
|
23
23
|
copy_file "harmonia_sync.rb", "app/models/harmonia/sync.rb"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def copy_concerns
|
|
27
|
+
copy_file "concerns/sync_loggable.rb", "app/syncers/concerns/sync_loggable.rb"
|
|
28
|
+
copy_file "concerns/sync_retryable.rb", "app/syncers/concerns/sync_retryable.rb"
|
|
29
|
+
copy_file "concerns/photo_syncable.rb", "app/syncers/concerns/photo_syncable.rb"
|
|
30
|
+
copy_file "concerns/related_model_validation.rb", "app/syncers/concerns/related_model_validation.rb"
|
|
31
|
+
copy_file "concerns/related_model_resolver.rb", "app/syncers/concerns/related_model_resolver.rb"
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
def generate_migration
|
|
27
35
|
migration_template "create_harmonia_syncs.rb", "db/migrate/create_harmonia_syncs.rb"
|
|
28
36
|
end
|
|
29
37
|
|
|
38
|
+
def generate_server_id_migration
|
|
39
|
+
migration_template "add_server_id_to_harmonia_syncs.rb", "db/migrate/add_server_id_to_harmonia_syncs.rb"
|
|
40
|
+
end
|
|
41
|
+
|
|
30
42
|
def generate_failed_ids_migration
|
|
31
43
|
migration_template "add_failed_ids_to_harmonia_syncs.rb", "db/migrate/add_failed_ids_to_harmonia_syncs.rb"
|
|
32
44
|
end
|
|
@@ -43,24 +55,28 @@ module Harmonia
|
|
|
43
55
|
- config/initializers/trophonius_model_extension.rb
|
|
44
56
|
- app/models/application_record.rb (with to_fm extension)
|
|
45
57
|
- app/models/harmonia/sync.rb
|
|
58
|
+
- app/syncers/concerns/sync_loggable.rb
|
|
59
|
+
- app/syncers/concerns/sync_retryable.rb
|
|
60
|
+
- app/syncers/concerns/photo_syncable.rb
|
|
61
|
+
- app/syncers/concerns/related_model_validation.rb
|
|
62
|
+
- app/syncers/concerns/related_model_resolver.rb
|
|
46
63
|
- db/migrate/..._create_harmonia_syncs.rb
|
|
64
|
+
- db/migrate/..._add_server_id_to_harmonia_syncs.rb
|
|
47
65
|
- db/migrate/..._add_failed_ids_to_harmonia_syncs.rb
|
|
48
66
|
|
|
49
67
|
Next steps:
|
|
50
68
|
1. Run migrations: rails db:migrate
|
|
51
69
|
2. Update database_connector.rb with your FileMaker database name
|
|
52
70
|
3. Add FileMaker credentials to Rails credentials
|
|
53
|
-
4.
|
|
54
|
-
|
|
55
|
-
- rails generate harmonia:
|
|
56
|
-
- rails generate harmonia:reverse_sync ModelName (ActiveRecord -> FileMaker)
|
|
71
|
+
4. Generate syncers:
|
|
72
|
+
- rails generate harmonia:sync ModelName (FileMaker -> ActiveRecord)
|
|
73
|
+
- rails generate harmonia:reverse_sync ModelName (ActiveRecord -> FileMaker)
|
|
57
74
|
|
|
58
75
|
README
|
|
59
76
|
|
|
60
77
|
say readme_content, :green if behavior == :invoke
|
|
61
78
|
end
|
|
62
79
|
|
|
63
|
-
# Required for migration_template to work
|
|
64
80
|
def self.next_migration_number(dirname)
|
|
65
81
|
current = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
66
82
|
|
|
@@ -34,7 +34,7 @@ module Harmonia
|
|
|
34
34
|
task #{task_name}: :environment do
|
|
35
35
|
dbc = DatabaseConnector.new
|
|
36
36
|
|
|
37
|
-
syncer = #{class_name}
|
|
37
|
+
syncer = #{class_name}ToFileMakerSyncer.new(dbc)
|
|
38
38
|
syncer.run
|
|
39
39
|
end
|
|
40
40
|
TASK
|
|
@@ -61,7 +61,10 @@ module Harmonia
|
|
|
61
61
|
|
|
62
62
|
desc 'sync #{table_name} from ActiveRecord to FileMaker'
|
|
63
63
|
task #{task_name}: :environment do
|
|
64
|
-
|
|
64
|
+
dbc = DatabaseConnector.new
|
|
65
|
+
|
|
66
|
+
syncer = #{class_name}ToFileMakerSyncer.new(dbc)
|
|
67
|
+
syncer.run
|
|
65
68
|
end
|
|
66
69
|
TASK
|
|
67
70
|
|
|
@@ -32,7 +32,11 @@ module Harmonia
|
|
|
32
32
|
|
|
33
33
|
desc 'sync #{table_name} from FileMaker to ActiveRecord'
|
|
34
34
|
task #{task_name}: :environment do
|
|
35
|
-
|
|
35
|
+
dbc = DatabaseConnector.new
|
|
36
|
+
server_id = ENV.fetch('SERVER_ID', nil)&.to_i
|
|
37
|
+
|
|
38
|
+
syncer = #{class_name}Syncer.new(dbc, server_id)
|
|
39
|
+
syncer.run
|
|
36
40
|
end
|
|
37
41
|
TASK
|
|
38
42
|
|
|
@@ -59,8 +63,9 @@ module Harmonia
|
|
|
59
63
|
desc 'sync #{table_name} from FileMaker to ActiveRecord'
|
|
60
64
|
task #{task_name}: :environment do
|
|
61
65
|
dbc = DatabaseConnector.new
|
|
66
|
+
server_id = ENV.fetch('SERVER_ID', nil)&.to_i
|
|
62
67
|
|
|
63
|
-
syncer = #{class_name}Syncer.new(dbc)
|
|
68
|
+
syncer = #{class_name}Syncer.new(dbc, server_id)
|
|
64
69
|
syncer.run
|
|
65
70
|
end
|
|
66
71
|
TASK
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class <%= class_name %>ToFileMakerSyncer
|
|
4
|
+
include SyncLoggable
|
|
5
|
+
|
|
4
6
|
attr_accessor :database_connector
|
|
5
7
|
|
|
6
8
|
def initialize(database_connector)
|
|
7
9
|
@database_connector = database_connector
|
|
8
|
-
@last_synced_on = Harmonia::Sync.last_sync_for('<%= table_name %>', 'ActiveRecord to FileMaker')&.ran_on || (Time.now - 15.years)
|
|
10
|
+
@last_synced_on = Harmonia::Sync.last_sync_for('<%= table_name %>', 'ActiveRecord to FileMaker', nil)&.ran_on || (Time.now - 15.years)
|
|
11
|
+
@failed_fm_ids = {}
|
|
12
|
+
@failed_pg_ids = {}
|
|
9
13
|
end
|
|
10
14
|
|
|
11
|
-
# Main sync method
|
|
12
|
-
# Executes the sync process for <%= class_name %> records to FileMaker
|
|
13
15
|
def run
|
|
14
16
|
raise StandardError, 'No database connector set' if @database_connector.blank?
|
|
15
17
|
|
|
16
18
|
sync_record = create_sync_record
|
|
19
|
+
@sync_started_at = Time.now
|
|
20
|
+
log_sync_start(table: '<%= table_name %>', direction: 'ActiveRecord to FileMaker')
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
sync_records(sync_record)
|
|
21
|
-
end
|
|
22
|
+
sync_record.start!
|
|
23
|
+
sync_records(sync_record)
|
|
22
24
|
rescue StandardError => e
|
|
23
|
-
|
|
25
|
+
log_sync_error(table: '<%= table_name %>', direction: 'ActiveRecord to FileMaker', error: e)
|
|
26
|
+
sync_record&.fail!(e.message, failed_fm_ids: @failed_fm_ids, failed_pg_ids: @failed_pg_ids)
|
|
24
27
|
raise
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -31,57 +34,55 @@ class <%= class_name %>ToFileMakerSyncer
|
|
|
31
34
|
created_count = create_records
|
|
32
35
|
delete_records
|
|
33
36
|
|
|
34
|
-
total_synced
|
|
37
|
+
total_synced = created_count + updated_count
|
|
35
38
|
total_required = (@total_create_required || 0) + (@total_update_required || 0)
|
|
36
39
|
|
|
40
|
+
log_sync_plan(
|
|
41
|
+
table: '<%= table_name %>', direction: 'ActiveRecord to FileMaker',
|
|
42
|
+
to_create: @total_create_required || 0,
|
|
43
|
+
to_update: @total_update_required || 0,
|
|
44
|
+
to_delete: 0
|
|
45
|
+
)
|
|
46
|
+
|
|
37
47
|
sync_record.finish!(
|
|
38
|
-
records_synced:
|
|
39
|
-
records_required: total_required
|
|
48
|
+
records_synced: total_synced,
|
|
49
|
+
records_required: total_required,
|
|
50
|
+
failed_fm_ids: @failed_fm_ids,
|
|
51
|
+
failed_pg_ids: @failed_pg_ids
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
log_sync_finish(
|
|
55
|
+
table: '<%= table_name %>', direction: 'ActiveRecord to FileMaker',
|
|
56
|
+
created: created_count, updated: updated_count, deleted: 0,
|
|
57
|
+
failed_fm: @failed_fm_ids.size, failed_pg: @failed_pg_ids.size,
|
|
58
|
+
duration: Time.now - @sync_started_at
|
|
40
59
|
)
|
|
41
60
|
end
|
|
42
61
|
|
|
43
|
-
# Returns
|
|
44
|
-
#
|
|
45
|
-
# Set @total_create_required to the total number of records that should exist after creation
|
|
46
|
-
# @return [Array<<%= class_name %>>] Array of ActiveRecord records
|
|
62
|
+
# Returns ActiveRecord records that need to be created in FileMaker (no filemaker_id yet).
|
|
63
|
+
# Set @total_create_required to the count.
|
|
47
64
|
def records_to_create
|
|
48
|
-
# TODO:
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# existing_ids = YourTrophoniusModel.all.map { |r| r.field_data['PostgreSQLID'] }
|
|
53
|
-
# pg_records.reject { |record| existing_ids.include?(record.id.to_s) }
|
|
65
|
+
# TODO: implement
|
|
66
|
+
# pg_records = <%= class_name %>.where(filemaker_id: nil, ...)
|
|
67
|
+
# @total_create_required = pg_records.count
|
|
68
|
+
# pg_records
|
|
54
69
|
@total_create_required = 0
|
|
55
70
|
[]
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
# Returns
|
|
59
|
-
#
|
|
60
|
-
# Set @total_update_required to the total number of records that should be updated
|
|
61
|
-
# @return [Array<<%= class_name %>>] Array of ActiveRecord records
|
|
73
|
+
# Returns ActiveRecord records that need to be updated in FileMaker.
|
|
74
|
+
# Set @total_update_required to the count.
|
|
62
75
|
def records_to_update
|
|
63
|
-
# TODO:
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# fm_record = find_filemaker_record(pg_record)
|
|
68
|
-
# fm_record && needs_update?(pg_record, fm_record)
|
|
69
|
-
# }
|
|
70
|
-
# @total_update_required = records_needing_update.length
|
|
71
|
-
# records_needing_update
|
|
76
|
+
# TODO: implement
|
|
77
|
+
# pg_records = <%= class_name %>.where('updated_at > ?', @last_synced_on).where.not(filemaker_id: nil)
|
|
78
|
+
# @total_update_required = pg_records.count
|
|
79
|
+
# pg_records
|
|
72
80
|
@total_update_required = 0
|
|
73
81
|
[]
|
|
74
82
|
end
|
|
75
83
|
|
|
76
|
-
# Returns
|
|
77
|
-
# @return [Array] Array of FileMaker record IDs
|
|
84
|
+
# Returns FileMaker record IDs that should be deleted.
|
|
78
85
|
def records_to_delete
|
|
79
|
-
# TODO: Implement logic to determine which FileMaker records should be deleted
|
|
80
|
-
# Example:
|
|
81
|
-
# pg_ids = <%= class_name %>.pluck(:id).map(&:to_s)
|
|
82
|
-
# YourTrophoniusModel.all.select { |fm_record|
|
|
83
|
-
# !pg_ids.include?(fm_record.field_data['PostgreSQLID'])
|
|
84
|
-
# }.map(&:record_id)
|
|
85
86
|
[]
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -89,71 +90,71 @@ class <%= class_name %>ToFileMakerSyncer
|
|
|
89
90
|
records = records_to_create
|
|
90
91
|
return 0 if records.empty?
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
success_count = 0
|
|
94
|
+
@database_connector.open_database do
|
|
95
|
+
records.each do |pg_record|
|
|
96
|
+
fm_attributes = pg_record.to_fm.compact_blank
|
|
97
|
+
fm_record = FileMaker::<%= class_name %>.create(fm_attributes)
|
|
98
|
+
pg_record.update!(filemaker_id: fm_record.id)
|
|
99
|
+
success_count += 1
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
@failed_pg_ids[pg_record.id.to_s] = e.message
|
|
102
|
+
log_create_error(id: pg_record.id, id_type: :pg, error: e)
|
|
103
|
+
end
|
|
95
104
|
end
|
|
96
|
-
|
|
97
|
-
records.size
|
|
105
|
+
success_count
|
|
98
106
|
end
|
|
99
107
|
|
|
100
108
|
def update_records
|
|
101
109
|
records = records_to_update
|
|
102
110
|
return 0 if records.empty?
|
|
103
111
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
success_count = 0
|
|
113
|
+
@database_connector.open_database do
|
|
114
|
+
records.each do |pg_record|
|
|
115
|
+
fm_record = find_filemaker_record(pg_record)
|
|
116
|
+
next unless fm_record
|
|
117
|
+
|
|
118
|
+
fm_record.update(pg_record.to_fm.compact_blank)
|
|
119
|
+
success_count += 1
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
@failed_pg_ids[pg_record.id.to_s] = e.message
|
|
122
|
+
log_update_error(id: pg_record.id, id_type: :pg, error: e)
|
|
123
|
+
end
|
|
112
124
|
end
|
|
113
|
-
|
|
114
|
-
records.size
|
|
125
|
+
success_count
|
|
115
126
|
end
|
|
116
127
|
|
|
117
128
|
def delete_records
|
|
118
|
-
|
|
119
|
-
return if
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
ids = records_to_delete
|
|
130
|
+
return if ids.empty?
|
|
131
|
+
|
|
132
|
+
@database_connector.open_database do
|
|
133
|
+
ids.each do |id|
|
|
134
|
+
FileMaker::<%= class_name %>.where(id:).first&.destroy
|
|
135
|
+
rescue Trophonius::RecordNotFoundError
|
|
136
|
+
next
|
|
137
|
+
end
|
|
127
138
|
end
|
|
128
139
|
end
|
|
129
140
|
|
|
130
|
-
#
|
|
131
|
-
# @param pg_record [ActiveRecord::Base] The PostgreSQL record
|
|
132
|
-
# @return [Trophonius::Model, nil] The corresponding FileMaker record or nil
|
|
141
|
+
# Find the FileMaker record corresponding to a PostgreSQL record.
|
|
133
142
|
def find_filemaker_record(pg_record)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
nil
|
|
143
|
+
return nil if pg_record.filemaker_id.blank?
|
|
144
|
+
|
|
145
|
+
FileMaker::<%= class_name %>.where(id: pg_record.filemaker_id).first
|
|
138
146
|
end
|
|
139
147
|
|
|
140
|
-
# Determine if a FileMaker record needs to be updated based on PostgreSQL record
|
|
141
|
-
# @param pg_record [ActiveRecord::Base] The PostgreSQL record
|
|
142
|
-
# @param fm_record [Trophonius::Model] The FileMaker record
|
|
143
|
-
# @return [Boolean] true if update is needed
|
|
144
148
|
def needs_update?(pg_record, fm_record)
|
|
145
|
-
# TODO:
|
|
146
|
-
# Example:
|
|
147
|
-
# fm_attributes = pg_record.to_fm
|
|
148
|
-
# fm_attributes.any? { |key, value| fm_record.field_data[key.to_s] != value }
|
|
149
|
+
# TODO: implement comparison logic
|
|
149
150
|
true
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
def create_sync_record
|
|
153
154
|
Harmonia::Sync.create!(
|
|
154
|
-
table:
|
|
155
|
-
ran_on:
|
|
156
|
-
status:
|
|
155
|
+
table: '<%= table_name %>',
|
|
156
|
+
ran_on: Time.now,
|
|
157
|
+
status: 'pending',
|
|
157
158
|
direction: 'ActiveRecord to FileMaker'
|
|
158
159
|
)
|
|
159
160
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddServerIdToHarmoniaSyncs < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
unless column_exists?(:harmonia_syncs, :server_id)
|
|
6
|
+
add_column :harmonia_syncs, :server_id, :integer
|
|
7
|
+
add_index :harmonia_syncs, :server_id
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
|
|
3
|
+
module PhotoSyncable
|
|
4
|
+
def sync_photos(pg_records, fm_records, options: {})
|
|
5
|
+
attachment = options[:attachment] || 'photo'
|
|
6
|
+
container_field = options[:container_field] || 'foto'
|
|
7
|
+
filename_field = options[:filename_field] || 'foto_filename'
|
|
8
|
+
purge_existing = options[:purge_existing]
|
|
9
|
+
|
|
10
|
+
pg_records.each do |record|
|
|
11
|
+
fm_record = fm_records.find { |r| r.id == record.filemaker_id }
|
|
12
|
+
record.send(attachment).purge if record.send(attachment).attached? && purge_existing
|
|
13
|
+
next unless fm_record.send(container_field).present?
|
|
14
|
+
|
|
15
|
+
downloaded_image = URI.parse(fm_record.send(container_field)).open
|
|
16
|
+
record.send(attachment).attach(io: downloaded_image, filename: fm_record.send(filename_field))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sync_photo_to_filemaker(attachment, fm_record, options: {})
|
|
21
|
+
container_field = options[:container_field] || 'foto'
|
|
22
|
+
return unless attachment.blank?
|
|
23
|
+
|
|
24
|
+
attachment.open do |tempfile|
|
|
25
|
+
fm_record.upload(container_name: container_field, file: tempfile)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RelatedModelResolver
|
|
4
|
+
# FM models extend this and define related_models to declare which FM fields
|
|
5
|
+
# must resolve to a local PG record. Returns a hash of resolved IDs, or nil
|
|
6
|
+
# if any required reference cannot be resolved.
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# def self.related_models
|
|
10
|
+
# [[:sales_region_id, ::SalesRegion], [:administration_id, ::Administration]]
|
|
11
|
+
# end
|
|
12
|
+
def resolve_related_models(record)
|
|
13
|
+
related_models.each_with_object({}) do |(field, model), hash|
|
|
14
|
+
resolved = model.find_by(filemaker_id: record.send(field))&.id
|
|
15
|
+
return nil if resolved.nil?
|
|
16
|
+
|
|
17
|
+
hash[field] = resolved
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RelatedModelValidation
|
|
4
|
+
def validate_related_models(record, fm_model_class)
|
|
5
|
+
references = fm_model_class.related_models.map { |field, model| [record.send(field), model] }
|
|
6
|
+
errors = references.filter_map do |fm_id, model|
|
|
7
|
+
label = model.name.demodulize.underscore
|
|
8
|
+
if fm_id.blank?
|
|
9
|
+
"#{label} is blank in FileMaker"
|
|
10
|
+
elsif model.find_by(filemaker_id: fm_id).nil?
|
|
11
|
+
"#{label} with FileMaker id '#{fm_id}' not found in PostgreSQL"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
"Skipped: #{errors.join(', ')}" if errors.any?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SyncLoggable
|
|
4
|
+
def log_sync_start(table:, direction:, server_id: nil)
|
|
5
|
+
parts = ["[SYNC START] #{table} | #{direction}"]
|
|
6
|
+
parts << "server=#{server_id}" if server_id
|
|
7
|
+
Rails.logger.info(parts.join(' | '))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def log_sync_plan(table:, direction:, to_create:, to_update:, to_delete:)
|
|
11
|
+
Rails.logger.info(
|
|
12
|
+
"[SYNC PLAN ] #{table} | #{direction} | " \
|
|
13
|
+
"to_create=#{to_create} to_update=#{to_update} to_delete=#{to_delete}"
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def log_sync_finish(table:, direction:, created:, updated:, deleted:, failed_fm:, failed_pg:, duration: nil)
|
|
18
|
+
parts = [
|
|
19
|
+
"[SYNC DONE ] #{table} | #{direction}",
|
|
20
|
+
"created=#{created} updated=#{updated} deleted=#{deleted}",
|
|
21
|
+
"failed_fm=#{failed_fm} failed_pg=#{failed_pg}"
|
|
22
|
+
]
|
|
23
|
+
parts << "duration=#{duration.round(2)}s" if duration
|
|
24
|
+
Rails.logger.info(parts.join(' | '))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log_sync_error(table:, direction:, error:)
|
|
28
|
+
Rails.logger.error("[SYNC ERROR] #{table} | #{direction} | #{error.class}: #{error.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def log_create_error(id:, id_type: :fm, error:)
|
|
32
|
+
label = id_type == :fm ? "FileMaker ID" : "PostgreSQL ID"
|
|
33
|
+
Rails.logger.error("[SYNC] Failed to create record from #{label} #{id}: #{error.message}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def log_update_error(id:, id_type: :fm, error:)
|
|
37
|
+
label = id_type == :fm ? "FileMaker ID" : "PostgreSQL ID"
|
|
38
|
+
Rails.logger.error("[SYNC] Failed to update record from #{label} #{id}: #{error.message}")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def log_delete_error(id:, id_type: :fm, error:)
|
|
42
|
+
label = id_type == :fm ? "FileMaker ID" : "PostgreSQL ID"
|
|
43
|
+
Rails.logger.error("[SYNC] Failed to delete record with #{label} #{id}: #{error.message}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def log_needs_update_error(id:, id_type: :fm, error:)
|
|
47
|
+
label = id_type == :fm ? "FileMaker ID" : "PostgreSQL ID"
|
|
48
|
+
Rails.logger.error("[SYNC] Failed needs_update? check for #{label} #{id}: #{error.message}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adds retry capability for FM IDs that failed in the previous sync run.
|
|
4
|
+
#
|
|
5
|
+
# Usage in a syncer:
|
|
6
|
+
# include SyncRetryable
|
|
7
|
+
#
|
|
8
|
+
# def run
|
|
9
|
+
# ...
|
|
10
|
+
# @database_connector.open_database do
|
|
11
|
+
# sync_record.start!
|
|
12
|
+
# sync_records(sync_record)
|
|
13
|
+
# retry_failed_records(FileMaker::MyModel, table: 'my_models', direction: 'FileMaker to ActiveRecord')
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# The concern reads failed_fm_ids from the previous completed/failed sync,
|
|
18
|
+
# fetches those records from FileMaker, then calls retry_create / retry_update.
|
|
19
|
+
# Syncers can override either hook for custom logic (e.g. photo handling).
|
|
20
|
+
|
|
21
|
+
module SyncRetryable
|
|
22
|
+
def retry_failed_records(fm_model_class, table:, direction:)
|
|
23
|
+
previous_sync = Harmonia::Sync
|
|
24
|
+
.where(table:, direction:, server_id: @server_id)
|
|
25
|
+
.where(status: %w[completed failed])
|
|
26
|
+
.order(ran_on: :desc)
|
|
27
|
+
.offset(1) # skip the sync that just completed
|
|
28
|
+
.first
|
|
29
|
+
|
|
30
|
+
return unless previous_sync
|
|
31
|
+
|
|
32
|
+
failed_fm_ids = previous_sync.failed_fm_ids.keys
|
|
33
|
+
return if failed_fm_ids.empty?
|
|
34
|
+
|
|
35
|
+
log_sync_retry_start(table:, direction:, count: failed_fm_ids.size, ids: failed_fm_ids)
|
|
36
|
+
|
|
37
|
+
fm_records = failed_fm_ids.filter_map do |fm_id|
|
|
38
|
+
fm_model_class.where(id: fm_id).first
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
Rails.logger.warn("[SYNC RETRY] Could not fetch FM record #{fm_id}: #{e.message}")
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
pg_model = retryable_pg_model
|
|
45
|
+
to_create = fm_records.reject { |r| pg_model.exists?(filemaker_id: r.id) }
|
|
46
|
+
to_update = fm_records.select { |r| pg_model.exists?(filemaker_id: r.id) }
|
|
47
|
+
|
|
48
|
+
created = retry_create(to_create, fm_model_class)
|
|
49
|
+
updated = retry_update(to_update, fm_model_class)
|
|
50
|
+
|
|
51
|
+
log_sync_retry_finish(table:, direction:, created:, updated:)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Override in the syncer for custom create logic (e.g. photo sync, insert_all).
|
|
57
|
+
def retry_create(fm_records, fm_model_class)
|
|
58
|
+
return 0 if fm_records.empty?
|
|
59
|
+
|
|
60
|
+
pg_model = retryable_pg_model
|
|
61
|
+
success = 0
|
|
62
|
+
fm_records.each do |trophonius_record|
|
|
63
|
+
attributes = fm_model_class.to_pg(trophonius_record).merge(
|
|
64
|
+
created_at: Time.current, updated_at: Time.current
|
|
65
|
+
)
|
|
66
|
+
pg_model.create!(attributes)
|
|
67
|
+
@failed_fm_ids.delete(trophonius_record.id.to_s)
|
|
68
|
+
success += 1
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
@failed_fm_ids[trophonius_record.id.to_s] = e.message
|
|
71
|
+
log_create_error(id: trophonius_record.id, error: e)
|
|
72
|
+
end
|
|
73
|
+
success
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Override in the syncer for custom update logic.
|
|
77
|
+
def retry_update(fm_records, fm_model_class)
|
|
78
|
+
return 0 if fm_records.empty?
|
|
79
|
+
|
|
80
|
+
pg_model = retryable_pg_model
|
|
81
|
+
success = 0
|
|
82
|
+
fm_records.each do |trophonius_record|
|
|
83
|
+
attributes = fm_model_class.to_pg(trophonius_record)
|
|
84
|
+
pg_model.where(filemaker_id: trophonius_record.id).update_all(
|
|
85
|
+
attributes.merge(updated_at: Time.current)
|
|
86
|
+
)
|
|
87
|
+
@failed_fm_ids.delete(trophonius_record.id.to_s)
|
|
88
|
+
success += 1
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
@failed_fm_ids[trophonius_record.id.to_s] = e.message
|
|
91
|
+
log_update_error(id: trophonius_record.id, error: e)
|
|
92
|
+
end
|
|
93
|
+
success
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Infers the PG model from the syncer class name. Override if naming diverges.
|
|
97
|
+
# AssetSyncer → Asset, AssetDefectSyncer → AssetDefect, etc.
|
|
98
|
+
def retryable_pg_model
|
|
99
|
+
self.class.name.delete_suffix('Syncer').constantize
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def log_sync_retry_start(table:, direction:, count:, ids:)
|
|
103
|
+
Rails.logger.info(
|
|
104
|
+
"[SYNC RETRY] #{table} | #{direction} | retrying #{count} record(s) | ids=#{ids.join(',')}"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def log_sync_retry_finish(table:, direction:, created:, updated:)
|
|
109
|
+
Rails.logger.info(
|
|
110
|
+
"[SYNC RETRY] #{table} | #{direction} | created=#{created} updated=#{updated}"
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -10,6 +10,7 @@ class CreateHarmoniaSyncs < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>
|
|
|
10
10
|
t.string :status, default: 'pending'
|
|
11
11
|
t.string :direction
|
|
12
12
|
t.text :error_message
|
|
13
|
+
t.integer :server_id
|
|
13
14
|
t.jsonb :failed_fm_ids, default: {}
|
|
14
15
|
t.jsonb :failed_pg_ids, default: {}
|
|
15
16
|
|
|
@@ -18,5 +19,6 @@ class CreateHarmoniaSyncs < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>
|
|
|
18
19
|
|
|
19
20
|
add_index :harmonia_syncs, [:table, :ran_on]
|
|
20
21
|
add_index :harmonia_syncs, :status
|
|
22
|
+
add_index :harmonia_syncs, :server_id
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class <%= class_name %>Syncer
|
|
4
|
+
include SyncLoggable
|
|
5
|
+
include SyncRetryable
|
|
6
|
+
|
|
4
7
|
attr_accessor :database_connector
|
|
5
8
|
|
|
6
|
-
def initialize(database_connector)
|
|
9
|
+
def initialize(database_connector, server_id, update_only: false)
|
|
7
10
|
@database_connector = database_connector
|
|
8
|
-
@
|
|
11
|
+
@server_id = server_id
|
|
12
|
+
@last_synced_on = if update_only
|
|
13
|
+
Time.now - 100.years
|
|
14
|
+
else
|
|
15
|
+
Harmonia::Sync.last_sync_for('<%= table_name %>', 'FileMaker to ActiveRecord', server_id)&.ran_on || (Time.now - 15.years)
|
|
16
|
+
end
|
|
17
|
+
@update_only = update_only
|
|
9
18
|
@failed_fm_ids = {}
|
|
10
19
|
@failed_pg_ids = {}
|
|
11
20
|
end
|
|
12
21
|
|
|
13
|
-
# Main sync method
|
|
14
|
-
# Executes the sync process for <%= class_name %> records
|
|
15
22
|
def run
|
|
16
23
|
raise StandardError, 'No database connector set' if @database_connector.blank?
|
|
17
24
|
|
|
18
25
|
sync_record = create_sync_record
|
|
26
|
+
@sync_started_at = Time.now
|
|
27
|
+
log_sync_start(table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord', server_id: @server_id)
|
|
19
28
|
|
|
20
29
|
@database_connector.open_database do
|
|
21
30
|
sync_record.start!
|
|
22
31
|
sync_records(sync_record)
|
|
32
|
+
retry_failed_records(FileMaker::<%= class_name %>, table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord')
|
|
23
33
|
end
|
|
24
34
|
rescue StandardError => e
|
|
35
|
+
log_sync_error(table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord', error: e)
|
|
25
36
|
sync_record&.fail!(e.message, failed_fm_ids: @failed_fm_ids, failed_pg_ids: @failed_pg_ids)
|
|
26
37
|
raise
|
|
27
38
|
end
|
|
@@ -30,78 +41,77 @@ class <%= class_name %>Syncer
|
|
|
30
41
|
|
|
31
42
|
def sync_records(sync_record)
|
|
32
43
|
updated_count = update_records
|
|
33
|
-
created_count = create_records
|
|
34
|
-
delete_records
|
|
44
|
+
created_count = @update_only ? 0 : create_records
|
|
45
|
+
deleted_count = @update_only ? 0 : delete_records.to_i
|
|
35
46
|
|
|
36
|
-
total_synced
|
|
47
|
+
total_synced = created_count + updated_count
|
|
37
48
|
total_required = (@total_create_required || 0) + (@total_update_required || 0)
|
|
38
49
|
|
|
50
|
+
log_sync_plan(
|
|
51
|
+
table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord',
|
|
52
|
+
to_create: @total_create_required || 0,
|
|
53
|
+
to_update: @total_update_required || 0,
|
|
54
|
+
to_delete: deleted_count
|
|
55
|
+
)
|
|
56
|
+
|
|
39
57
|
sync_record.finish!(
|
|
40
|
-
records_synced:
|
|
58
|
+
records_synced: total_synced,
|
|
41
59
|
records_required: total_required,
|
|
42
|
-
failed_fm_ids:
|
|
43
|
-
failed_pg_ids:
|
|
60
|
+
failed_fm_ids: @failed_fm_ids,
|
|
61
|
+
failed_pg_ids: @failed_pg_ids
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
log_sync_finish(
|
|
65
|
+
table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord',
|
|
66
|
+
created: created_count, updated: updated_count, deleted: deleted_count,
|
|
67
|
+
failed_fm: @failed_fm_ids.size, failed_pg: @failed_pg_ids.size,
|
|
68
|
+
duration: Time.now - @sync_started_at
|
|
44
69
|
)
|
|
45
70
|
end
|
|
46
71
|
|
|
47
|
-
# Returns
|
|
48
|
-
#
|
|
49
|
-
# Set @total_create_required to the total number of records that should exist after creation
|
|
50
|
-
# @return [Array<Trophonius::Record>] Array of Trophonius records
|
|
72
|
+
# Returns FileMaker records that need to be created in PostgreSQL.
|
|
73
|
+
# Set @total_create_required to the count of records that should be created.
|
|
51
74
|
def records_to_create
|
|
52
|
-
# TODO:
|
|
53
|
-
# Example:
|
|
54
|
-
filemaker_records = FileMaker::<%= class_name %>.where(creation_timestamp: ">= #{@last_synced_on.to_fm}").not
|
|
55
|
-
@total_create_required = filemaker_records.length
|
|
75
|
+
# TODO: implement
|
|
56
76
|
existing_ids = <%= class_name %>.pluck(:filemaker_id)
|
|
57
|
-
filemaker_records
|
|
77
|
+
filemaker_records = FileMaker::<%= class_name %>.where(creation_timestamp: ">= #{@last_synced_on.to_fm}")
|
|
78
|
+
filemaker_records.reject! { |record| existing_ids.include?(record.id) }
|
|
79
|
+
@total_create_required = filemaker_records.length
|
|
80
|
+
filemaker_records
|
|
58
81
|
end
|
|
59
82
|
|
|
60
|
-
# Returns
|
|
61
|
-
#
|
|
62
|
-
# Set @total_update_required to the total number of records that should be updated
|
|
63
|
-
# @return [Array<Trophonius::Record>] Array of Trophonius records
|
|
83
|
+
# Returns FileMaker records that need to be updated in PostgreSQL.
|
|
84
|
+
# Set @total_update_required to the count of records that should be updated.
|
|
64
85
|
def records_to_update
|
|
65
|
-
# TODO:
|
|
66
|
-
# Example:
|
|
86
|
+
# TODO: implement
|
|
67
87
|
filemaker_records = FileMaker::<%= class_name %>.where(modification_timestamp: ">= #{@last_synced_on.to_fm}")
|
|
68
|
-
records_needing_update = filemaker_records.select
|
|
69
|
-
pg_record = <%= class_name %>.find_by(filemaker_id: fm_record.
|
|
88
|
+
records_needing_update = filemaker_records.select do |fm_record|
|
|
89
|
+
pg_record = <%= class_name %>.find_by(filemaker_id: fm_record.id)
|
|
70
90
|
pg_record && needs_update?(fm_record, pg_record)
|
|
71
|
-
|
|
91
|
+
end
|
|
72
92
|
@total_update_required = records_needing_update.length
|
|
73
93
|
records_needing_update
|
|
74
94
|
end
|
|
75
95
|
|
|
76
|
-
# Returns
|
|
77
|
-
# @return [Array] Array of record identifiers
|
|
96
|
+
# Returns PostgreSQL IDs of records that have been deleted in FileMaker.
|
|
78
97
|
def records_to_delete
|
|
79
|
-
# Get all modified FileMaker record IDs
|
|
80
98
|
filemaker_records = FileMaker::<%= class_name %>.where(modification_timestamp: ">= #{@last_synced_on.to_fm}")
|
|
81
|
-
fm_ids = filemaker_records.map(&:
|
|
99
|
+
fm_ids = filemaker_records.map(&:id)
|
|
82
100
|
|
|
83
|
-
# Find PostgreSQL records whose FileMaker IDs aren't in the modified set
|
|
84
|
-
# These might have been deleted in FileMaker
|
|
85
101
|
fm_ids_no_update_needed = <%= class_name %>.where.not(filemaker_id: fm_ids).pluck(:filemaker_id)
|
|
86
102
|
return [] if fm_ids_no_update_needed.empty?
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
possibly_deleted_query = FileMaker::<%= class_name %>.where(record_id: fm_ids_no_update_needed.first)
|
|
104
|
+
possibly_deleted_query = FileMaker::<%= class_name %>.where(id: fm_ids_no_update_needed.first)
|
|
90
105
|
fm_ids_no_update_needed.count > 1 && fm_ids_no_update_needed[1..].each do |fm_id|
|
|
91
|
-
possibly_deleted_query.or(
|
|
106
|
+
possibly_deleted_query.or(id: fm_id)
|
|
92
107
|
end
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
deleted_fm_ids = fm_ids_no_update_needed - possibly_deleted_query.map(&:record_id)
|
|
96
|
-
|
|
97
|
-
# Return PostgreSQL IDs for records with these FileMaker IDs
|
|
109
|
+
deleted_fm_ids = fm_ids_no_update_needed - possibly_deleted_query.map(&:id)
|
|
98
110
|
<%= class_name %>.where(filemaker_id: deleted_fm_ids).pluck(:id)
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
def needs_update?(fm_record, pg_record)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
pg_attributes.any? { |key, value| pg_record.send(key) != value }
|
|
114
|
+
FileMaker::<%= class_name %>.to_pg(fm_record).any? { |key, value| pg_record.send(key) != value }
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
def create_records
|
|
@@ -109,21 +119,17 @@ class <%= class_name %>Syncer
|
|
|
109
119
|
return 0 if records.empty?
|
|
110
120
|
|
|
111
121
|
success_count = 0
|
|
112
|
-
|
|
113
122
|
records.each do |trophonius_record|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Rails.logger.error("Failed to create record from FileMaker ID #{trophonius_record.record_id}: #{e.message}")
|
|
124
|
-
end
|
|
123
|
+
attributes = FileMaker::<%= class_name %>.to_pg(trophonius_record).merge(
|
|
124
|
+
created_at: Time.current,
|
|
125
|
+
updated_at: Time.current
|
|
126
|
+
)
|
|
127
|
+
<%= class_name %>.create!(attributes)
|
|
128
|
+
success_count += 1
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
@failed_fm_ids[trophonius_record.id.to_s] = e.message
|
|
131
|
+
log_create_error(id: trophonius_record.id, error: e)
|
|
125
132
|
end
|
|
126
|
-
|
|
127
133
|
success_count
|
|
128
134
|
end
|
|
129
135
|
|
|
@@ -132,21 +138,16 @@ class <%= class_name %>Syncer
|
|
|
132
138
|
return 0 if records.empty?
|
|
133
139
|
|
|
134
140
|
success_count = 0
|
|
135
|
-
|
|
136
141
|
records.each do |trophonius_record|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@failed_fm_ids[trophonius_record.record_id.to_s] = e.message
|
|
146
|
-
Rails.logger.error("Failed to update record from FileMaker ID #{trophonius_record.record_id}: #{e.message}")
|
|
147
|
-
end
|
|
142
|
+
pg_attributes = FileMaker::<%= class_name %>.to_pg(trophonius_record)
|
|
143
|
+
<%= class_name %>.where(filemaker_id: trophonius_record.id).update_all(
|
|
144
|
+
pg_attributes.merge(updated_at: Time.current)
|
|
145
|
+
)
|
|
146
|
+
success_count += 1
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
@failed_fm_ids[trophonius_record.id.to_s] = e.message
|
|
149
|
+
log_update_error(id: trophonius_record.id, error: e)
|
|
148
150
|
end
|
|
149
|
-
|
|
150
151
|
success_count
|
|
151
152
|
end
|
|
152
153
|
|
|
@@ -155,20 +156,19 @@ class <%= class_name %>Syncer
|
|
|
155
156
|
return if ids.empty?
|
|
156
157
|
|
|
157
158
|
ids.each do |pg_id|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Rails.logger.error("Failed to delete record with PostgreSQL ID #{pg_id}: #{e.message}")
|
|
163
|
-
end
|
|
159
|
+
<%= class_name %>.where(id: pg_id).destroy_all
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
@failed_pg_ids[pg_id.to_s] = e.message
|
|
162
|
+
log_delete_error(id: pg_id, id_type: :pg, error: e)
|
|
164
163
|
end
|
|
165
164
|
end
|
|
166
165
|
|
|
167
166
|
def create_sync_record
|
|
168
167
|
Harmonia::Sync.create!(
|
|
169
|
-
table:
|
|
170
|
-
ran_on:
|
|
171
|
-
status:
|
|
168
|
+
table: '<%= table_name %>',
|
|
169
|
+
ran_on: Time.now,
|
|
170
|
+
status: 'pending',
|
|
171
|
+
server_id: @server_id,
|
|
172
172
|
direction: 'FileMaker to ActiveRecord'
|
|
173
173
|
)
|
|
174
174
|
end
|
|
@@ -8,44 +8,36 @@ module Harmonia
|
|
|
8
8
|
validates :ran_on, presence: true
|
|
9
9
|
validates :status, presence: true, inclusion: { in: %w[pending in_progress completed failed] }
|
|
10
10
|
|
|
11
|
-
# Scope to get syncs for a specific table
|
|
12
11
|
scope :for_table, ->(table_name) { where(table: table_name) }
|
|
13
|
-
scope :for_direction, ->(direction) {where(direction:)}
|
|
14
|
-
|
|
15
|
-
# Scope to get recent syncs
|
|
12
|
+
scope :for_direction, ->(direction) { where(direction:) }
|
|
13
|
+
scope :for_server, ->(server_id) { where(server_id:) }
|
|
16
14
|
scope :recent, -> { order(ran_on: :desc) }
|
|
17
|
-
|
|
18
|
-
# Scope to get syncs by date
|
|
19
15
|
scope :on_date, ->(date) { where(ran_on: date) }
|
|
20
|
-
|
|
21
|
-
# Scope by status
|
|
22
16
|
scope :pending, -> { where(status: 'pending') }
|
|
23
17
|
scope :in_progress, -> { where(status: 'in_progress') }
|
|
24
18
|
scope :completed, -> { where(status: 'completed') }
|
|
25
19
|
scope :failed, -> { where(status: 'failed') }
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
def self.last_sync_for(table_name, direction, server_id = nil)
|
|
22
|
+
scope = completed.for_direction(direction).for_table(table_name)
|
|
23
|
+
scope = server_id ? scope.for_server(server_id) : scope.where(server_id: nil)
|
|
24
|
+
scope.recent.first
|
|
30
25
|
end
|
|
31
26
|
|
|
32
|
-
# Calculate sync completion percentage
|
|
33
27
|
def completion_percentage
|
|
34
28
|
return 0 if records_required.to_i.zero?
|
|
29
|
+
|
|
35
30
|
((records_synced.to_f / records_required.to_f) * 100).round(2)
|
|
36
31
|
end
|
|
37
32
|
|
|
38
|
-
# Check if sync was complete
|
|
39
33
|
def complete?
|
|
40
34
|
status == 'completed' && records_synced == records_required
|
|
41
35
|
end
|
|
42
36
|
|
|
43
|
-
# Mark sync as started
|
|
44
37
|
def start!
|
|
45
38
|
update!(status: 'in_progress')
|
|
46
39
|
end
|
|
47
40
|
|
|
48
|
-
# Mark sync as completed
|
|
49
41
|
def finish!(records_synced:, records_required:, failed_fm_ids: {}, failed_pg_ids: {})
|
|
50
42
|
status = records_synced == records_required ? 'completed' : 'failed'
|
|
51
43
|
update!(
|
|
@@ -57,7 +49,6 @@ module Harmonia
|
|
|
57
49
|
)
|
|
58
50
|
end
|
|
59
51
|
|
|
60
|
-
# Mark sync as failed
|
|
61
52
|
def fail!(error_message, failed_fm_ids: {}, failed_pg_ids: {})
|
|
62
53
|
update!(
|
|
63
54
|
status: 'failed',
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: harmonia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kempen Automatisering
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: trophonius
|
|
@@ -39,7 +39,13 @@ files:
|
|
|
39
39
|
- lib/generators/harmonia/templates/activerecord_to_filemaker_syncer_template.rb
|
|
40
40
|
- lib/generators/harmonia/templates/add_failed_ids_to_harmonia_syncs.rb
|
|
41
41
|
- lib/generators/harmonia/templates/add_filemaker_id_to_table.rb
|
|
42
|
+
- lib/generators/harmonia/templates/add_server_id_to_harmonia_syncs.rb
|
|
42
43
|
- lib/generators/harmonia/templates/application_record_extension.rb
|
|
44
|
+
- lib/generators/harmonia/templates/concerns/photo_syncable.rb
|
|
45
|
+
- lib/generators/harmonia/templates/concerns/related_model_resolver.rb
|
|
46
|
+
- lib/generators/harmonia/templates/concerns/related_model_validation.rb
|
|
47
|
+
- lib/generators/harmonia/templates/concerns/sync_loggable.rb
|
|
48
|
+
- lib/generators/harmonia/templates/concerns/sync_retryable.rb
|
|
43
49
|
- lib/generators/harmonia/templates/create_harmonia_syncs.rb
|
|
44
50
|
- lib/generators/harmonia/templates/database_connector.rb
|
|
45
51
|
- lib/generators/harmonia/templates/filemaker_to_activerecord_syncer_template.rb
|