harmonia 0.1.7
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 +7 -0
- data/License.txt +7 -0
- data/README.md +666 -0
- data/lib/generators/harmonia/install_generator.rb +64 -0
- data/lib/generators/harmonia/reverse_sync_generator.rb +86 -0
- data/lib/generators/harmonia/sync_generator.rb +73 -0
- data/lib/generators/harmonia/templates/activerecord_to_filemaker_syncer_template.rb +160 -0
- data/lib/generators/harmonia/templates/add_filemaker_id_to_table.rb +10 -0
- data/lib/generators/harmonia/templates/application_record_extension.rb +13 -0
- data/lib/generators/harmonia/templates/create_harmonia_syncs.rb +20 -0
- data/lib/generators/harmonia/templates/database_connector.rb +34 -0
- data/lib/generators/harmonia/templates/filemaker_to_activerecord_syncer_template.rb +137 -0
- data/lib/generators/harmonia/templates/harmonia_sync.rb +65 -0
- data/lib/generators/harmonia/templates/trophonius_model_extension.rb +11 -0
- data/lib/harmonia/railtie.rb +10 -0
- data/lib/harmonia.rb +9 -0
- metadata +73 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harmonia
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
include Rails::Generators::Migration
|
|
7
|
+
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def copy_database_connector
|
|
11
|
+
copy_file "database_connector.rb", "app/services/database_connector.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy_trophonius_extension
|
|
15
|
+
copy_file "trophonius_model_extension.rb", "config/initializers/trophonius_model_extension.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_application_record_extension
|
|
19
|
+
copy_file "application_record_extension.rb", "app/models/application_record.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def copy_sync_model
|
|
23
|
+
copy_file "harmonia_sync.rb", "app/models/harmonia/sync.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def generate_migration
|
|
27
|
+
migration_template "create_harmonia_syncs.rb", "db/migrate/create_harmonia_syncs.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def show_readme
|
|
31
|
+
readme_content = <<~README
|
|
32
|
+
|
|
33
|
+
========================================
|
|
34
|
+
Harmonia has been installed!
|
|
35
|
+
========================================
|
|
36
|
+
|
|
37
|
+
Files created:
|
|
38
|
+
- app/services/database_connector.rb
|
|
39
|
+
- config/initializers/trophonius_model_extension.rb
|
|
40
|
+
- app/models/application_record.rb (with to_fm extension)
|
|
41
|
+
- app/models/harmonia/sync.rb
|
|
42
|
+
- db/migrate/..._create_harmonia_syncs.rb
|
|
43
|
+
|
|
44
|
+
Next steps:
|
|
45
|
+
1. Run migrations: rails db:migrate
|
|
46
|
+
2. Update database_connector.rb with your FileMaker database name
|
|
47
|
+
3. Add FileMaker credentials to Rails credentials
|
|
48
|
+
4. Replace all instances of YourTrophoniusModel with your actual model names
|
|
49
|
+
5. Generate syncers:
|
|
50
|
+
- rails generate harmonia:sync ModelName (FileMaker -> ActiveRecord)
|
|
51
|
+
- rails generate harmonia:reverse_sync ModelName (ActiveRecord -> FileMaker)
|
|
52
|
+
|
|
53
|
+
README
|
|
54
|
+
|
|
55
|
+
say readme_content, :green if behavior == :invoke
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Required for migration_template to work
|
|
59
|
+
def self.next_migration_number(dirname)
|
|
60
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harmonia
|
|
4
|
+
module Generators
|
|
5
|
+
class ReverseSyncGenerator < Rails::Generators::NamedBase
|
|
6
|
+
include Rails::Generators::Migration
|
|
7
|
+
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
argument :name, type: :string, required: true, banner: "ModelName"
|
|
11
|
+
|
|
12
|
+
def create_sync_file
|
|
13
|
+
template "activerecord_to_filemaker_syncer_template.rb", "app/syncers/#{file_name}_to_filemaker_syncer.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate_migration
|
|
17
|
+
migration_template "add_filemaker_id_to_table.rb", "db/migrate/add_filemaker_id_to_#{table_name}.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def show_readme
|
|
21
|
+
readme_content = <<~README
|
|
22
|
+
|
|
23
|
+
========================================
|
|
24
|
+
#{class_name}ToFileMakerSyncer has been generated!
|
|
25
|
+
========================================
|
|
26
|
+
|
|
27
|
+
Files created:
|
|
28
|
+
- app/syncers/#{file_name}_to_filemaker_syncer.rb
|
|
29
|
+
- db/migrate/..._add_filemaker_id_to_#{table_name}.rb
|
|
30
|
+
|
|
31
|
+
Next steps:
|
|
32
|
+
1. Run migrations: rails db:migrate
|
|
33
|
+
|
|
34
|
+
2. Implement the #{class_name}.to_fm method in your model:
|
|
35
|
+
class #{class_name} < ApplicationRecord
|
|
36
|
+
def self.to_fm(record)
|
|
37
|
+
{
|
|
38
|
+
'FieldMakerFieldName' => record.attribute_name,
|
|
39
|
+
# ... map other fields
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
3. Implement the records_to_create method
|
|
45
|
+
- Set @total_create_required to the total number of records that should be created
|
|
46
|
+
- Return an array of ActiveRecord records to create in FileMaker
|
|
47
|
+
|
|
48
|
+
4. Implement the records_to_update method
|
|
49
|
+
- Set @total_update_required to the total number of records that should be updated
|
|
50
|
+
- Return an array of ActiveRecord records to update in FileMaker
|
|
51
|
+
|
|
52
|
+
5. Implement the find_filemaker_record method
|
|
53
|
+
- Find corresponding FileMaker record for a given ActiveRecord record
|
|
54
|
+
|
|
55
|
+
6. Implement the records_to_delete method (optional)
|
|
56
|
+
- Return an array of FileMaker record IDs to delete
|
|
57
|
+
|
|
58
|
+
Note: The total_required count used for sync tracking is automatically calculated
|
|
59
|
+
from @total_create_required + @total_update_required
|
|
60
|
+
|
|
61
|
+
README
|
|
62
|
+
|
|
63
|
+
say readme_content, :green if behavior == :invoke
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Required for migration_template to work
|
|
67
|
+
def self.next_migration_number(dirname)
|
|
68
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def file_name
|
|
74
|
+
name.underscore
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def class_name
|
|
78
|
+
name.camelize
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def table_name
|
|
82
|
+
name.underscore.pluralize
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harmonia
|
|
4
|
+
module Generators
|
|
5
|
+
class SyncGenerator < Rails::Generators::NamedBase
|
|
6
|
+
include Rails::Generators::Migration
|
|
7
|
+
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
argument :name, type: :string, required: true, banner: "ModelName"
|
|
11
|
+
|
|
12
|
+
def create_sync_file
|
|
13
|
+
template "filemaker_to_activerecord_syncer_template.rb", "app/syncers/#{file_name}_syncer.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate_migration
|
|
17
|
+
migration_template "add_filemaker_id_to_table.rb", "db/migrate/add_filemaker_id_to_#{table_name}.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def show_readme
|
|
21
|
+
readme_content = <<~README
|
|
22
|
+
|
|
23
|
+
========================================
|
|
24
|
+
#{class_name}Syncer has been generated!
|
|
25
|
+
========================================
|
|
26
|
+
|
|
27
|
+
Files created:
|
|
28
|
+
- app/syncers/#{file_name}_syncer.rb
|
|
29
|
+
- db/migrate/..._add_filemaker_id_to_#{table_name}.rb
|
|
30
|
+
|
|
31
|
+
Next steps:
|
|
32
|
+
1. Run migrations: rails db:migrate
|
|
33
|
+
|
|
34
|
+
2. Implement the records_to_create method
|
|
35
|
+
- Set @total_create_required to the total number of records that should be created
|
|
36
|
+
- Return an array of Trophonius records to create
|
|
37
|
+
|
|
38
|
+
3. Implement the records_to_update method
|
|
39
|
+
- Set @total_update_required to the total number of records that should be updated
|
|
40
|
+
- Return an array of Trophonius records to update
|
|
41
|
+
|
|
42
|
+
4. Implement the records_to_delete method (optional)
|
|
43
|
+
- Return an array of record identifiers to delete
|
|
44
|
+
|
|
45
|
+
Note: The total_required count used for sync tracking is automatically calculated
|
|
46
|
+
from @total_create_required + @total_update_required
|
|
47
|
+
|
|
48
|
+
README
|
|
49
|
+
|
|
50
|
+
say readme_content, :green if behavior == :invoke
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Required for migration_template to work
|
|
54
|
+
def self.next_migration_number(dirname)
|
|
55
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def file_name
|
|
61
|
+
name.underscore
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def class_name
|
|
65
|
+
name.camelize
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def table_name
|
|
69
|
+
name.underscore.pluralize
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>ToFileMakerSyncer
|
|
4
|
+
attr_accessor :database_connector
|
|
5
|
+
|
|
6
|
+
def initialize(database_connector)
|
|
7
|
+
@database_connector = database_connector
|
|
8
|
+
@last_synced_on = Harmonia::Sync.last_sync_for('<%= table_name %>', 'ActiveRecord to FileMaker')&.ran_on || (Time.now - 15.years)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Main sync method
|
|
12
|
+
# Executes the sync process for <%= class_name %> records to FileMaker
|
|
13
|
+
def run
|
|
14
|
+
raise StandardError, 'No database connector set' if @database_connector.blank?
|
|
15
|
+
|
|
16
|
+
sync_record = create_sync_record
|
|
17
|
+
|
|
18
|
+
@database_connector.open_database do
|
|
19
|
+
sync_record.start!
|
|
20
|
+
sync_records(sync_record)
|
|
21
|
+
end
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
sync_record&.fail!(e.message)
|
|
24
|
+
raise
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def sync_records(sync_record)
|
|
30
|
+
updated_count = update_records
|
|
31
|
+
created_count = create_records
|
|
32
|
+
delete_records
|
|
33
|
+
|
|
34
|
+
total_synced = created_count + updated_count
|
|
35
|
+
total_required = (@total_create_required || 0) + (@total_update_required || 0)
|
|
36
|
+
|
|
37
|
+
sync_record.finish!(
|
|
38
|
+
records_synced: total_synced,
|
|
39
|
+
records_required: total_required
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns an array of ActiveRecord records that need to be created in FileMaker
|
|
44
|
+
# Use <%= class_name %>.to_fm(record) to convert to FileMaker attributes
|
|
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
|
|
47
|
+
def records_to_create
|
|
48
|
+
# TODO: Implement logic to fetch records from PostgreSQL that need to be created in FileMaker
|
|
49
|
+
# Example:
|
|
50
|
+
# pg_records = <%= class_name %>.all
|
|
51
|
+
# @total_create_required = pg_records.length
|
|
52
|
+
# existing_ids = YourTrophoniusModel.all.map { |r| r.field_data['PostgreSQLID'] }
|
|
53
|
+
# pg_records.reject { |record| existing_ids.include?(record.id.to_s) }
|
|
54
|
+
@total_create_required = 0
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns an array of ActiveRecord records that need to be updated in FileMaker
|
|
59
|
+
# Use <%= class_name %>.to_fm(record) to convert to FileMaker attributes
|
|
60
|
+
# Set @total_update_required to the total number of records that should be updated
|
|
61
|
+
# @return [Array<<%= class_name %>>] Array of ActiveRecord records
|
|
62
|
+
def records_to_update
|
|
63
|
+
# TODO: Implement logic to fetch records from PostgreSQL that need to be updated in FileMaker
|
|
64
|
+
# Example:
|
|
65
|
+
# pg_records = <%= class_name %>.where('updated_at > ?', 1.hour.ago)
|
|
66
|
+
# records_needing_update = pg_records.select { |pg_record|
|
|
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
|
|
72
|
+
@total_update_required = 0
|
|
73
|
+
[]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns an array of FileMaker record IDs that need to be deleted
|
|
77
|
+
# @return [Array] Array of FileMaker record IDs
|
|
78
|
+
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
|
+
end
|
|
87
|
+
|
|
88
|
+
def create_records
|
|
89
|
+
records = records_to_create
|
|
90
|
+
return 0 if records.empty?
|
|
91
|
+
|
|
92
|
+
records.each do |pg_record|
|
|
93
|
+
fm_attributes = pg_record.to_fm
|
|
94
|
+
YourTrophoniusModel.create(fm_attributes)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
records.size
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def update_records
|
|
101
|
+
records = records_to_update
|
|
102
|
+
return 0 if records.empty?
|
|
103
|
+
|
|
104
|
+
records.each do |pg_record|
|
|
105
|
+
fm_attributes = pg_record.to_fm
|
|
106
|
+
|
|
107
|
+
# Find the FileMaker record by PostgreSQL ID or other unique identifier
|
|
108
|
+
fm_record = find_filemaker_record(pg_record)
|
|
109
|
+
next unless fm_record
|
|
110
|
+
|
|
111
|
+
fm_record.update(fm_attributes)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
records.size
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def delete_records
|
|
118
|
+
record_ids = records_to_delete
|
|
119
|
+
return if record_ids.empty?
|
|
120
|
+
|
|
121
|
+
record_ids.each do |record_id|
|
|
122
|
+
fm_record = YourTrophoniusModel.find(record_id)
|
|
123
|
+
fm_record.destroy
|
|
124
|
+
rescue Trophonius::RecordNotFoundError
|
|
125
|
+
# Record already deleted, skip
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Helper method to find a FileMaker record based on a PostgreSQL record
|
|
131
|
+
# @param pg_record [ActiveRecord::Base] The PostgreSQL record
|
|
132
|
+
# @return [Trophonius::Model, nil] The corresponding FileMaker record or nil
|
|
133
|
+
def find_filemaker_record(pg_record)
|
|
134
|
+
# TODO: Implement logic to find the corresponding FileMaker record
|
|
135
|
+
# Example:
|
|
136
|
+
# YourTrophoniusModel.find_by_field('PostgreSQLID', pg_record.id.to_s)
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
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
|
+
def needs_update?(pg_record, fm_record)
|
|
145
|
+
# TODO: Implement your comparison logic
|
|
146
|
+
# Example:
|
|
147
|
+
# fm_attributes = pg_record.to_fm
|
|
148
|
+
# fm_attributes.any? { |key, value| fm_record.field_data[key.to_s] != value }
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def create_sync_record
|
|
153
|
+
Harmonia::Sync.create!(
|
|
154
|
+
table: '<%= table_name %>',
|
|
155
|
+
ran_on: Time.now,
|
|
156
|
+
status: 'pending',
|
|
157
|
+
direction: 'ActiveRecord to FileMaker'
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddFilemakerIdTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
unless column_exists?(:<%= table_name %>, :filemaker_id)
|
|
6
|
+
add_column :<%= table_name %>, :filemaker_id, :string
|
|
7
|
+
add_index :<%= table_name %>, :filemaker_id, unique: true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
4
|
+
primary_abstract_class
|
|
5
|
+
|
|
6
|
+
# Converts an ActiveRecord record to FileMaker-compatible attributes
|
|
7
|
+
# This method should be overridden in each model that syncs to FileMaker
|
|
8
|
+
# @param record [ActiveRecord::Base] The ActiveRecord record instance to convert
|
|
9
|
+
# @return [Hash] Hash of FileMaker field names and values
|
|
10
|
+
def to_fm(record)
|
|
11
|
+
raise NotImplementedError, "#{self.class.name}#to_fm must be implemented to convert ActiveRecord records to FileMaker attributes"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateHarmoniaSyncs < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :harmonia_syncs do |t|
|
|
6
|
+
t.datetime :ran_on
|
|
7
|
+
t.string :table
|
|
8
|
+
t.integer :records_synced, default: 0
|
|
9
|
+
t.integer :records_required, default: 0
|
|
10
|
+
t.string :status, default: 'pending'
|
|
11
|
+
t.string :direction
|
|
12
|
+
t.text :error_message
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :harmonia_syncs, [:table, :ran_on]
|
|
18
|
+
add_index :harmonia_syncs, :status
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DatabaseConnector
|
|
4
|
+
attr_accessor :hostname
|
|
5
|
+
|
|
6
|
+
def open_database(&block)
|
|
7
|
+
raise ArgumentError, 'No hostname set' if @hostname.blank?
|
|
8
|
+
raise ArgumentError, 'No block given' if block.blank?
|
|
9
|
+
|
|
10
|
+
connect
|
|
11
|
+
yield block
|
|
12
|
+
ensure
|
|
13
|
+
disconnect
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def connect
|
|
19
|
+
Trophonius.configure do |config|
|
|
20
|
+
config.host = @hostname
|
|
21
|
+
config.database = 'DatabaseName'
|
|
22
|
+
config.username = Rails.application.credentials.dig(:filemaker, :username)
|
|
23
|
+
config.password = Rails.application.credentials.dig(:filemaker, :password)
|
|
24
|
+
config.ssl = true # or false depending on whether https or http should be used
|
|
25
|
+
config.debug = true # will output more information when true
|
|
26
|
+
config.pool_size = ENV.fetch('trophonius_pool', 5) # use multiple data api connections with a loadbalancer to improve performance
|
|
27
|
+
end
|
|
28
|
+
@connection_manager = Trophonius.connection_manager
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def disconnect
|
|
32
|
+
@connection_manager.disconnect_all
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>Syncer
|
|
4
|
+
attr_accessor :database_connector
|
|
5
|
+
|
|
6
|
+
def initialize(database_connector)
|
|
7
|
+
@database_connector = database_connector
|
|
8
|
+
@last_synced_on = Harmonia::Sync.last_sync_for('<%= table_name %>', 'FileMaker to ActiveRecord')&.ran_on || (Time.now - 15.year)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Main sync method
|
|
12
|
+
# Executes the sync process for <%= class_name %> records
|
|
13
|
+
def run
|
|
14
|
+
raise StandardError, 'No database connector set' if @database_connector.blank?
|
|
15
|
+
|
|
16
|
+
sync_record = create_sync_record
|
|
17
|
+
|
|
18
|
+
@database_connector.open_database do
|
|
19
|
+
sync_record.start!
|
|
20
|
+
sync_records(sync_record)
|
|
21
|
+
end
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
sync_record&.fail!(e.message)
|
|
24
|
+
raise
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def sync_records(sync_record)
|
|
30
|
+
updated_count = update_records
|
|
31
|
+
created_count = create_records
|
|
32
|
+
delete_records
|
|
33
|
+
|
|
34
|
+
total_synced = created_count + updated_count
|
|
35
|
+
total_required = (@total_create_required || 0) + (@total_update_required || 0)
|
|
36
|
+
|
|
37
|
+
sync_record.finish!(
|
|
38
|
+
records_synced: total_synced,
|
|
39
|
+
records_required: total_required
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns an array of Trophonius records that need to be created
|
|
44
|
+
# Use YourTrophoniusModel.to_pg(record) to convert to PostgreSQL attributes
|
|
45
|
+
# Set @total_create_required to the total number of records that should exist after creation
|
|
46
|
+
# @return [Array<Trophonius::Record>] Array of Trophonius records
|
|
47
|
+
def records_to_create
|
|
48
|
+
# TODO: Implement logic to fetch records from FileMaker that need to be created in PostgreSQL
|
|
49
|
+
# Example:
|
|
50
|
+
# filemaker_records = YourTrophoniusModel.all
|
|
51
|
+
# @total_create_required = filemaker_records.length
|
|
52
|
+
# existing_ids = <%= class_name %>.pluck(:filemaker_id)
|
|
53
|
+
# filemaker_records.reject { |record| existing_ids.include?(record.record_id) }
|
|
54
|
+
@total_create_required = 0
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns an array of Trophonius records that need to be updated
|
|
59
|
+
# Use YourTrophoniusModel.to_pg(record) to convert to PostgreSQL attributes
|
|
60
|
+
# Set @total_update_required to the total number of records that should be updated
|
|
61
|
+
# @return [Array<Trophonius::Record>] Array of Trophonius records
|
|
62
|
+
def records_to_update
|
|
63
|
+
# TODO: Implement logic to fetch records from FileMaker that need to be updated in PostgreSQL
|
|
64
|
+
# Example:
|
|
65
|
+
# filemaker_records = YourTrophoniusModel.all
|
|
66
|
+
# records_needing_update = filemaker_records.select { |fm_record|
|
|
67
|
+
# pg_record = <%= class_name %>.find_by(filemaker_id: fm_record.record_id)
|
|
68
|
+
# pg_record && needs_update?(fm_record, pg_record)
|
|
69
|
+
# }
|
|
70
|
+
# @total_update_required = records_needing_update.length
|
|
71
|
+
# records_needing_update
|
|
72
|
+
@total_update_required = 0
|
|
73
|
+
[]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns an array of record identifiers that need to be deleted
|
|
77
|
+
# @return [Array] Array of record identifiers
|
|
78
|
+
def records_to_delete
|
|
79
|
+
# TODO: Implement logic to determine which PostgreSQL records should be deleted
|
|
80
|
+
# Example:
|
|
81
|
+
# filemaker_ids = YourTrophoniusModel.all.map(&:record_id)
|
|
82
|
+
# <%= class_name %>.where.not(filemaker_id: filemaker_ids).pluck(:id)
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def needs_update?(fm_record, pg_record)
|
|
87
|
+
pg_attributes = YourTrophoniusModel.to_pg(fm_record)
|
|
88
|
+
|
|
89
|
+
pg_attributes.any? { |key, value| pg_record.send(key) != value }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_records
|
|
93
|
+
records = records_to_create
|
|
94
|
+
return 0 if records.empty?
|
|
95
|
+
|
|
96
|
+
attributes_array = records.map do |trophonius_record|
|
|
97
|
+
YourTrophoniusModel.to_pg(trophonius_record).merge(
|
|
98
|
+
created_at: Time.current,
|
|
99
|
+
updated_at: Time.current
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
<%= class_name %>.insert_all(attributes_array)
|
|
104
|
+
records.size
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def update_records
|
|
108
|
+
records = records_to_update
|
|
109
|
+
return 0 if records.empty?
|
|
110
|
+
|
|
111
|
+
records.each do |trophonius_record|
|
|
112
|
+
pg_attributes = YourTrophoniusModel.to_pg(trophonius_record)
|
|
113
|
+
|
|
114
|
+
<%= class_name %>.where(filemaker_id: trophonius_record.record_id).update_all(
|
|
115
|
+
pg_attributes.merge(updated_at: Time.current)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
records.size
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def delete_records
|
|
123
|
+
ids = records_to_delete
|
|
124
|
+
return if ids.empty?
|
|
125
|
+
|
|
126
|
+
<%= class_name %>.where(id: ids).destroy_all
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def create_sync_record
|
|
130
|
+
Harmonia::Sync.create!(
|
|
131
|
+
table: '<%= table_name %>',
|
|
132
|
+
ran_on: Time.now,
|
|
133
|
+
status: 'pending',
|
|
134
|
+
direction: 'FileMaker to ActiveRecord'
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harmonia
|
|
4
|
+
class Sync < ApplicationRecord
|
|
5
|
+
self.table_name = 'harmonia_syncs'
|
|
6
|
+
|
|
7
|
+
validates :table, presence: true
|
|
8
|
+
validates :ran_on, presence: true
|
|
9
|
+
validates :status, presence: true, inclusion: { in: %w[pending in_progress completed failed] }
|
|
10
|
+
|
|
11
|
+
# Scope to get syncs for a specific table
|
|
12
|
+
scope :for_table, ->(table_name) { where(table: table_name) }
|
|
13
|
+
scope :for_direction, ->(direction) {where(direction:)}
|
|
14
|
+
|
|
15
|
+
# Scope to get recent syncs
|
|
16
|
+
scope :recent, -> { order(ran_on: :desc) }
|
|
17
|
+
|
|
18
|
+
# Scope to get syncs by date
|
|
19
|
+
scope :on_date, ->(date) { where(ran_on: date) }
|
|
20
|
+
|
|
21
|
+
# Scope by status
|
|
22
|
+
scope :pending, -> { where(status: 'pending') }
|
|
23
|
+
scope :in_progress, -> { where(status: 'in_progress') }
|
|
24
|
+
scope :completed, -> { where(status: 'completed') }
|
|
25
|
+
scope :failed, -> { where(status: 'failed') }
|
|
26
|
+
|
|
27
|
+
# Get the most recent sync for a table in a given direction
|
|
28
|
+
def self.last_sync_for(table_name, direction)
|
|
29
|
+
for_direction(direction).for_table(table_name).recent.first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Calculate sync completion percentage
|
|
33
|
+
def completion_percentage
|
|
34
|
+
return 0 if records_required.to_i.zero?
|
|
35
|
+
((records_synced.to_f / records_required.to_f) * 100).round(2)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if sync was complete
|
|
39
|
+
def complete?
|
|
40
|
+
status == 'completed' && records_synced == records_required
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Mark sync as started
|
|
44
|
+
def start!
|
|
45
|
+
update!(status: 'in_progress')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Mark sync as completed
|
|
49
|
+
def finish!(records_synced:, records_required:)
|
|
50
|
+
update!(
|
|
51
|
+
status: 'completed',
|
|
52
|
+
records_synced: records_synced,
|
|
53
|
+
records_required: records_required
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Mark sync as failed
|
|
58
|
+
def fail!(error_message)
|
|
59
|
+
update!(
|
|
60
|
+
status: 'failed',
|
|
61
|
+
error_message: error_message
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trophonius
|
|
4
|
+
class Model
|
|
5
|
+
# Converts a Trophonius record to PostgreSQL-compatible attributes
|
|
6
|
+
# @param record [Trophonius::Record] The Trophonius record instance to convert
|
|
7
|
+
def self.to_pg(record)
|
|
8
|
+
raise StandardError, 'Implement to_pg'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|