Harmonia is a Rails generator gem that creates synchronization logic between FileMaker databases and Ruby on Rails ActiveRecord models. It leverages the Trophonius gem for FileMaker Data API communication.
Harmonia is named after the Greek goddess of harmony and concord. In Greek mythology, Harmonia was the immortal goddess who reconciled opposing forces and brought them into balance. Just as the goddess brought harmony between different elements, this gem achieves harmony and concord between FileMaker databases and ActiveRecord databases, bridging two different data systems into a synchronized whole.
- Bidirectional Sync: Synchronize data from FileMaker to ActiveRecord and vice versa
- Automated Sync Generation: Generate syncer classes for your models with a single command
- Sync Tracking: Built-in model to track synchronization status, completion, and errors
- Flexible Architecture: Customize creation, update, and deletion logic for your specific needs
- Connection Management: Automatic FileMaker connection handling with proper cleanup
- Extensible: Override methods to implement custom sync logic
Add this line to your application's Gemfile:
gem 'harmonia'And then execute:
bundle installOr install it yourself as:
gem install harmoniaRun the install generator to set up the necessary files and database tables:
rails generate harmonia:installThis will create:
app/services/database_connector.rb- Manages FileMaker connectionsconfig/initializers/trophonius_model_extension.rb- Extends Trophonius models withto_pgmethodapp/models/application_record.rb- Extends ApplicationRecord withto_fmmethodapp/models/harmonia/sync.rb- Tracks sync operationsdb/migrate/[timestamp]_create_harmonia_syncs.rb- Migration for sync tracking table
After generation, remember to:
- Replace all instances of
YourTrophoniusModelwith your actual Trophonius model names - Update the
databaseconfiguration indatabase_connector.rb - Run the migration:
rails db:migrate
Add your FileMaker credentials to your Rails credentials file:
rails credentials:editAdd the following:
filemaker:
username: your_username
password: your_passwordDefine your FileMaker model using Trophonius. For example:
# app/models/trophonius/product.rb
module Trophonius
class Product < Trophonius::Model
config layout_name: 'ProductsLayout'
# Convert FileMaker record to PostgreSQL attributes
def self.to_pg(record)
{
filemaker_id: record.id,
name: record.product_name,
price: record.price,
sku: record.sku
}
end
end
endHarmonia supports two types of synchronization:
Generate a syncer that pulls data from FileMaker into your Rails database:
rails generate harmonia:sync ProductThis creates:
app/syncers/product_syncer.rb- The syncer classdb/migrate/[timestamp]_add_filemaker_id_to_products.rb- Migration to addfilemaker_idcolumn
Important: Run rails db:migrate after generation to add the filemaker_id column to your table. This column is used to track the FileMaker record ID and is automatically indexed for performance.
Generate a reverse syncer that pushes data from Rails to FileMaker:
rails generate harmonia:reverse_sync ProductThis creates:
app/syncers/product_to_filemaker_syncer.rb- The reverse syncer classdb/migrate/[timestamp]_add_filemaker_id_to_products.rb- Migration to addfilemaker_idcolumn (if not already present)
Important: Run rails db:migrate after generation. The filemaker_id column is used to maintain the relationship between ActiveRecord and FileMaker records.
Edit the generated syncer to implement your synchronization logic:
# app/syncers/product_syncer.rb
class ProductSyncer
attr_accessor :database_connector
def initialize(database_connector)
@database_connector = database_connector
end
def run
raise StandardError, 'No database connector set' if @database_connector.blank?
sync_record = create_sync_record
@database_connector.open_database do
sync_record.start!
sync_records(sync_record)
end
rescue StandardError => e
sync_record&.fail!(e.message)
raise
end
private
def records_to_create
filemaker_records = Trophonius::Product.all
@total_create_required = filemaker_records.length
existing_ids = Product.pluck(:filemaker_id)
filemaker_records.reject { |record| existing_ids.include?(record.record_id) }
end
def records_to_update
filemaker_records = Trophonius::Product.all
records_needing_update = filemaker_records.select { |fm_record|
pg_record = Product.find_by(filemaker_id: fm_record.record_id)
pg_record && needs_update?(fm_record, pg_record)
}
@total_update_required = records_needing_update.length
records_needing_update
end
def records_to_delete
filemaker_ids = Trophonius::Product.all.map(&:record_id)
Product.where.not(filemaker_id: filemaker_ids).pluck(:id)
end
def needs_update?(fm_record, pg_record)
# Implement your comparison logic
fm_data = Trophonius::Product.to_pg(fm_record)
pg_record.name != fm_data[:name] || pg_record.price != fm_data[:price]
end
# ... other methods (create_records, update_records, etc.)
endFor reverse sync, implement the to_fm method in your model and the syncer logic:
# app/models/product.rb
class Product < ApplicationRecord
# Convert ActiveRecord record to FileMaker attributes
def self.to_fm(record)
{
'PostgreSQLID' => record.id.to_s,
'ProductName' => record.name,
'Price' => record.price.to_s,
'SKU' => record.sku
}
end
end# app/syncers/product_to_filemaker_syncer.rb
class ProductToFileMakerSyncer
attr_accessor :database_connector
def initialize(database_connector)
@database_connector = database_connector
end
def run
raise StandardError, 'No database connector set' if @database_connector.blank?
sync_record = create_sync_record
@database_connector.open_database do
sync_record.start!
sync_records(sync_record)
end
rescue StandardError => e
sync_record&.fail!(e.message)
raise
end
private
def records_to_create
pg_records = Product.all
@total_create_required = pg_records.length
existing_ids = Trophonius::Product.all.map { |r| r.field_data['PostgreSQLID'] }
pg_records.reject { |record| existing_ids.include?(record.id.to_s) }
end
def records_to_update
pg_records = Product.where('updated_at > ?', 1.hour.ago)
records_needing_update = pg_records.select { |pg_record|
fm_record = find_filemaker_record(pg_record)
fm_record && needs_update?(pg_record, fm_record)
}
@total_update_required = records_needing_update.length
records_needing_update
end
def find_filemaker_record(pg_record)
Trophonius::Product.find_by_field('PostgreSQLID', pg_record.id.to_s)
end
def needs_update?(pg_record, fm_record)
fm_attributes = Product.to_fm(pg_record)
fm_attributes.any? { |key, value| fm_record.field_data[key.to_s] != value }
end
# ... other methods (create_records, update_records, etc.)
end# Create a database connector
connector = DatabaseConnector.new
connector.hostname = 'your-filemaker-server.com'
# Initialize and run the syncer
syncer = ProductSyncer.new(connector)
syncer.run# Create a database connector
connector = DatabaseConnector.new
connector.hostname = 'your-filemaker-server.com'
# Initialize and run the reverse syncer
syncer = ProductToFileMakerSyncer.new(connector)
syncer.runManages connections to the FileMaker server using Trophonius:
connector = DatabaseConnector.new
connector.hostname = 'filemaker.example.com'
connector.open_database do
# Your FileMaker operations here
end
# Connection automatically closedEach syncer handles the synchronization logic for a specific model. Harmonia supports two directions:
FileMaker to ActiveRecord Syncers (ModelNameSyncer):
records_to_create: Returns Trophonius records that need to be created in PostgreSQL- Must set
@total_create_requiredto track total records
- Must set
records_to_update: Returns Trophonius records that need to be updated- Must set
@total_update_requiredto track total records
- Must set
records_to_delete: Returns PostgreSQL record IDs that should be deletedcreate_records: Bulk creates new records in PostgreSQLupdate_records: Updates existing PostgreSQL recordsdelete_records: Removes obsolete PostgreSQL records
ActiveRecord to FileMaker Syncers (ModelNameToFileMakerSyncer):
records_to_create: Returns ActiveRecord records that need to be created in FileMaker- Must set
@total_create_requiredto track total records
- Must set
records_to_update: Returns ActiveRecord records that need to be updated in FileMaker- Must set
@total_update_requiredto track total records
- Must set
records_to_delete: Returns FileMaker record IDs that should be deletedfind_filemaker_record: Finds corresponding FileMaker record for an ActiveRecord recordcreate_records: Creates new records in FileMakerupdate_records: Updates existing FileMaker recordsdelete_records: Removes obsolete FileMaker records
Tracks sync operations with the following attributes:
table- Name of the table being syncedran_on- Date the sync was runstatus- One of:pending,in_progress,completed,failedrecords_synced- Number of records successfully syncedrecords_required- Total number of records that should existerror_message- Error details if sync failed
Important: The records_required value is automatically calculated as:
total_required = (@total_create_required || 0) + (@total_update_required || 0)You must set @total_create_required in records_to_create and @total_update_required in records_to_update.
# Get the last sync for a table
Harmonia::Sync.last_sync_for('products')
# Check completion percentage
sync = Harmonia::Sync.last
sync.completion_percentage # => 98.5
# Check if sync was complete
sync.complete? # => true if all records synced
# Query by status
Harmonia::Sync.completed
Harmonia::Sync.failed
Harmonia::Sync.in_progressTrophonius Model Extension - The to_pg class method is required for FileMaker to ActiveRecord sync:
module Trophonius
class Product < Trophonius::Model
# Converts FileMaker record to PostgreSQL attributes
def self.to_pg(record)
{
filemaker_id: record.record_id,
name: record.field_data['ProductName'],
price: record.field_data['Price'].to_f,
# ... map other fields
}
end
end
endApplicationRecord Extension - The to_fm class method is required for ActiveRecord to FileMaker sync:
class Product < ApplicationRecord
# Converts ActiveRecord record to FileMaker attributes
def self.to_fm(record)
{
'PostgreSQLID' => record.id.to_s,
'ProductName' => record.name,
'Price' => record.price.to_s,
# ... map other fields
}
end
endWhen you generate a syncer (either direction), Harmonia automatically creates a migration that adds a filemaker_id column to your table:
add_column :products, :filemaker_id, :string
add_index :products, :filemaker_id, unique: trueThis column serves as the bridge between your ActiveRecord records and FileMaker records:
- FileMaker to ActiveRecord: Stores the FileMaker
record_idto identify which FileMaker record corresponds to each Rails record - ActiveRecord to FileMaker: Can store the FileMaker
record_idafter creation, or you can use a separate field in FileMaker (likePostgreSQLID) to maintain the relationship
Important Notes:
- The column is indexed with a unique constraint for performance and data integrity
- The migration uses
column_exists?to avoid errors if the column already exists - If you generate both sync directions for the same model, the migration will only add the column once
Implement needs_update? to define when a record should be updated:
def needs_update?(fm_record, pg_record)
fm_data = Trophonius::Product.to_pg(fm_record)
# Compare specific fields
pg_record.name != fm_data[:name] ||
pg_record.price != fm_data[:price] ||
pg_record.updated_at < 1.day.ago
endFor large datasets, consider processing in batches:
def create_records
records = records_to_create
return 0 if records.empty?
records.each_slice(1000) do |batch|
attributes_array = batch.map do |trophonius_record|
Trophonius::Product.to_pg(trophonius_record).merge(
created_at: Time.current,
updated_at: Time.current
)
end
Product.insert_all(attributes_array)
end
records.size
endUse a background job processor like Sidekiq:
class ProductSyncJob < ApplicationJob
queue_as :default
def perform
connector = DatabaseConnector.new
connector.hostname = ENV['FILEMAKER_HOST']
syncer = ProductSyncer.new(connector)
syncer.run
end
end
# Schedule with cron or similar
# 0 2 * * * # Every day at 2 AMConfigure different connectors for different databases:
class DatabaseConnector
attr_accessor :hostname, :database_name
def connect
Trophonius.configure do |config|
config.host = @hostname
config.database = @database_name
# ... other config
end
end
end
# Usage
connector = DatabaseConnector.new
connector.hostname = 'filemaker.example.com'
connector.database_name = 'Products'Syncers automatically handle errors and mark syncs as failed:
def run
sync_record = create_sync_record
@database_connector.open_database do
sync_record.start!
sync_records(sync_record)
end
rescue StandardError => e
sync_record&.fail!(e.message)
raise
endCheck failed syncs:
failed_syncs = Harmonia::Sync.failed.recent
failed_syncs.each do |sync|
puts "#{sync.table}: #{sync.error_message}"
endAdjust connection pool size for better performance:
# In database_connector.rb
config.pool_size = ENV.fetch('TROPHONIUS_POOL', 5)Set in your environment:
TROPHONIUS_POOL=10Enable or disable SSL for FileMaker connections:
config.ssl = true # Use HTTPS
config.ssl = false # Use HTTPEnable detailed logging:
config.debug = truerequire 'rails_helper'
RSpec.describe ProductSyncer do
let(:connector) { instance_double(DatabaseConnector) }
let(:syncer) { described_class.new(connector) }
describe '#records_to_create' do
it 'returns records not in PostgreSQL' do
# Mock FileMaker records
allow(Trophonius::Product).to receive(:all).and_return([
double(record_id: 1),
double(record_id: 2)
])
# Mock existing PostgreSQL records
allow(Product).to receive(:pluck).with(:filemaker_id).and_return([1])
result = syncer.send(:records_to_create)
expect(result.map(&:record_id)).to eq([2])
end
end
endProblem: Cannot connect to FileMaker server
Solutions:
- Verify hostname and credentials
- Check if FileMaker Data API is enabled
- Ensure SSL settings match your server configuration
- Check firewall rules allow connections to port 443 (HTTPS) or 80 (HTTP)
Problem: Not all records are syncing
Solutions:
- Verify
records_to_createandrecords_to_updatelogic - Check FileMaker layouts include all necessary fields
- Ensure
to_pgmapping is correct - Review sync completion percentage
Problem: Syncs are slow
Solutions:
- Increase
pool_sizein Trophonius configuration - Implement batch processing
- Use
insert_allinstead of individual inserts - Add database indexes on
filemaker_idcolumns - Consider partial syncs for large datasets
Bug reports and pull requests are welcome on GitHub at https://github.com/KA-HQ/Harmonia.
The gem is available as open source under the terms of the MIT License.
Developed by Kempen Automatisering
Built on top of the excellent Trophonius gem for FileMaker Data API integration.