harmonia 0.2.4 → 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/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 +82 -81
- 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 +68 -73
- 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
|
|
@@ -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(&: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
129
|
ids = records_to_delete
|
|
119
130
|
return if ids.empty?
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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,32 +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, update_only: false)
|
|
9
|
+
def initialize(database_connector, server_id, update_only: false)
|
|
7
10
|
@database_connector = database_connector
|
|
11
|
+
@server_id = server_id
|
|
8
12
|
@last_synced_on = if update_only
|
|
9
|
-
Time.now - 100.
|
|
13
|
+
Time.now - 100.years
|
|
10
14
|
else
|
|
11
|
-
Harmonia::Sync.last_sync_for('<%= table_name %>', 'FileMaker to ActiveRecord')&.ran_on || (Time.now - 15.
|
|
15
|
+
Harmonia::Sync.last_sync_for('<%= table_name %>', 'FileMaker to ActiveRecord', server_id)&.ran_on || (Time.now - 15.years)
|
|
12
16
|
end
|
|
13
17
|
@update_only = update_only
|
|
14
18
|
@failed_fm_ids = {}
|
|
15
19
|
@failed_pg_ids = {}
|
|
16
20
|
end
|
|
17
21
|
|
|
18
|
-
# Main sync method
|
|
19
|
-
# Executes the sync process for <%= class_name %> records
|
|
20
22
|
def run
|
|
21
23
|
raise StandardError, 'No database connector set' if @database_connector.blank?
|
|
22
24
|
|
|
23
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)
|
|
24
28
|
|
|
25
29
|
@database_connector.open_database do
|
|
26
30
|
sync_record.start!
|
|
27
31
|
sync_records(sync_record)
|
|
32
|
+
retry_failed_records(FileMaker::<%= class_name %>, table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord')
|
|
28
33
|
end
|
|
29
34
|
rescue StandardError => e
|
|
35
|
+
log_sync_error(table: '<%= table_name %>', direction: 'FileMaker to ActiveRecord', error: e)
|
|
30
36
|
sync_record&.fail!(e.message, failed_fm_ids: @failed_fm_ids, failed_pg_ids: @failed_pg_ids)
|
|
31
37
|
raise
|
|
32
38
|
end
|
|
@@ -36,77 +42,76 @@ class <%= class_name %>Syncer
|
|
|
36
42
|
def sync_records(sync_record)
|
|
37
43
|
updated_count = update_records
|
|
38
44
|
created_count = @update_only ? 0 : create_records
|
|
39
|
-
|
|
45
|
+
deleted_count = @update_only ? 0 : delete_records.to_i
|
|
40
46
|
|
|
41
|
-
total_synced
|
|
47
|
+
total_synced = created_count + updated_count
|
|
42
48
|
total_required = (@total_create_required || 0) + (@total_update_required || 0)
|
|
43
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
|
+
|
|
44
57
|
sync_record.finish!(
|
|
45
|
-
records_synced:
|
|
58
|
+
records_synced: total_synced,
|
|
46
59
|
records_required: total_required,
|
|
47
|
-
failed_fm_ids:
|
|
48
|
-
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
|
|
49
69
|
)
|
|
50
70
|
end
|
|
51
71
|
|
|
52
|
-
# Returns
|
|
53
|
-
#
|
|
54
|
-
# Set @total_create_required to the total number of records that should exist after creation
|
|
55
|
-
# @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.
|
|
56
74
|
def records_to_create
|
|
57
|
-
# TODO:
|
|
58
|
-
|
|
75
|
+
# TODO: implement
|
|
76
|
+
existing_ids = <%= class_name %>.pluck(:filemaker_id)
|
|
59
77
|
filemaker_records = FileMaker::<%= class_name %>.where(creation_timestamp: ">= #{@last_synced_on.to_fm}")
|
|
78
|
+
filemaker_records.reject! { |record| existing_ids.include?(record.id) }
|
|
60
79
|
@total_create_required = filemaker_records.length
|
|
61
|
-
|
|
62
|
-
filemaker_records.reject { |record| existing_ids.include?(record.id) }
|
|
80
|
+
filemaker_records
|
|
63
81
|
end
|
|
64
82
|
|
|
65
|
-
# Returns
|
|
66
|
-
#
|
|
67
|
-
# Set @total_update_required to the total number of records that should be updated
|
|
68
|
-
# @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.
|
|
69
85
|
def records_to_update
|
|
70
|
-
# TODO:
|
|
71
|
-
# Example:
|
|
86
|
+
# TODO: implement
|
|
72
87
|
filemaker_records = FileMaker::<%= class_name %>.where(modification_timestamp: ">= #{@last_synced_on.to_fm}")
|
|
73
|
-
records_needing_update = filemaker_records.select
|
|
88
|
+
records_needing_update = filemaker_records.select do |fm_record|
|
|
74
89
|
pg_record = <%= class_name %>.find_by(filemaker_id: fm_record.id)
|
|
75
90
|
pg_record && needs_update?(fm_record, pg_record)
|
|
76
|
-
|
|
91
|
+
end
|
|
77
92
|
@total_update_required = records_needing_update.length
|
|
78
93
|
records_needing_update
|
|
79
94
|
end
|
|
80
95
|
|
|
81
|
-
# Returns
|
|
82
|
-
# @return [Array] Array of record identifiers
|
|
96
|
+
# Returns PostgreSQL IDs of records that have been deleted in FileMaker.
|
|
83
97
|
def records_to_delete
|
|
84
|
-
# Get all modified FileMaker record IDs
|
|
85
98
|
filemaker_records = FileMaker::<%= class_name %>.where(modification_timestamp: ">= #{@last_synced_on.to_fm}")
|
|
86
99
|
fm_ids = filemaker_records.map(&:id)
|
|
87
100
|
|
|
88
|
-
# Find PostgreSQL records whose FileMaker IDs aren't in the modified set
|
|
89
|
-
# These might have been deleted in FileMaker
|
|
90
101
|
fm_ids_no_update_needed = <%= class_name %>.where.not(filemaker_id: fm_ids).pluck(:filemaker_id)
|
|
91
102
|
return [] if fm_ids_no_update_needed.empty?
|
|
92
103
|
|
|
93
|
-
# Query FileMaker to check if these records still exist
|
|
94
104
|
possibly_deleted_query = FileMaker::<%= class_name %>.where(id: fm_ids_no_update_needed.first)
|
|
95
105
|
fm_ids_no_update_needed.count > 1 && fm_ids_no_update_needed[1..].each do |fm_id|
|
|
96
106
|
possibly_deleted_query.or(id: fm_id)
|
|
97
107
|
end
|
|
98
108
|
|
|
99
|
-
# Find IDs that exist in PostgreSQL but not in FileMaker (truly deleted)
|
|
100
109
|
deleted_fm_ids = fm_ids_no_update_needed - possibly_deleted_query.map(&:id)
|
|
101
|
-
|
|
102
|
-
# Return PostgreSQL IDs for records with these FileMaker IDs
|
|
103
110
|
<%= class_name %>.where(filemaker_id: deleted_fm_ids).pluck(:id)
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
def needs_update?(fm_record, pg_record)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
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 }
|
|
110
115
|
end
|
|
111
116
|
|
|
112
117
|
def create_records
|
|
@@ -114,21 +119,17 @@ class <%= class_name %>Syncer
|
|
|
114
119
|
return 0 if records.empty?
|
|
115
120
|
|
|
116
121
|
success_count = 0
|
|
117
|
-
|
|
118
122
|
records.each do |trophonius_record|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Rails.logger.error("Failed to create record from FileMaker ID #{trophonius_record.id}: #{e.message}")
|
|
129
|
-
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)
|
|
130
132
|
end
|
|
131
|
-
|
|
132
133
|
success_count
|
|
133
134
|
end
|
|
134
135
|
|
|
@@ -137,21 +138,16 @@ class <%= class_name %>Syncer
|
|
|
137
138
|
return 0 if records.empty?
|
|
138
139
|
|
|
139
140
|
success_count = 0
|
|
140
|
-
|
|
141
141
|
records.each do |trophonius_record|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@failed_fm_ids[trophonius_record.id.to_s] = e.message
|
|
151
|
-
Rails.logger.error("Failed to update record from FileMaker ID #{trophonius_record.id}: #{e.message}")
|
|
152
|
-
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)
|
|
153
150
|
end
|
|
154
|
-
|
|
155
151
|
success_count
|
|
156
152
|
end
|
|
157
153
|
|
|
@@ -160,20 +156,19 @@ class <%= class_name %>Syncer
|
|
|
160
156
|
return if ids.empty?
|
|
161
157
|
|
|
162
158
|
ids.each do |pg_id|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Rails.logger.error("Failed to delete record with PostgreSQL ID #{pg_id}: #{e.message}")
|
|
168
|
-
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)
|
|
169
163
|
end
|
|
170
164
|
end
|
|
171
165
|
|
|
172
166
|
def create_sync_record
|
|
173
167
|
Harmonia::Sync.create!(
|
|
174
|
-
table:
|
|
175
|
-
ran_on:
|
|
176
|
-
status:
|
|
168
|
+
table: '<%= table_name %>',
|
|
169
|
+
ran_on: Time.now,
|
|
170
|
+
status: 'pending',
|
|
171
|
+
server_id: @server_id,
|
|
177
172
|
direction: 'FileMaker to ActiveRecord'
|
|
178
173
|
)
|
|
179
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
|