Filemaker
A Ruby wrapper to FileMaker XML API.
Installation
At your Gemfile:
gem 'filemaker'
Initializing the Server
Ensure you have Web Publishing Engine (XML Publishing) enabled. Please turn on SSL or credential won't be protected. Remember to also set the "Extended Privileges" to: fmxml
.
Configuration for initializing a server:
-
host
- IP or hostname -
account
- Please useENV
variable likeENV['FILEMAKER_ACCOUNT']
-
password
- Please useENV
variable likeENV['FILEMAKER_PASSWORD']
-
ssl
- Use{ verify: false }
if you are using FileMaker's unsigned certificate. -
ssl_verifypeer
- Default tofalse
-
ssl_verifyhost
- Default to0
-
log
- A choice ofsimple
,curl
andcurl_auth
.
server = Filemaker::Server.new do |config|
config.host = ENV['FILEMAKER_HOST']
config.account_name = ENV['FILEMAKER_ACCOUNT_NAME']
config.password = ENV['FILEMAKER_PASSWORD']
config.ssl = { verify: false }
config.log = curl
end
server.databases.all # Using -dbnames
server.database['candidates'].layouts.all # Using -layoutnames and -db=candidates
server.database['candidates'].scripts.all # Using -scriptnames and -db=candidates
api = server.db['candidates'].lay['profile']
api = server.db['candidates']['profile']
api = server.database['candidates'].layout['profile']
api.find(...)
Once you are able to grab the api
, you are golden and can make requests to read/write to FileMaker API.
Using the API
Filemaker::Api::QueryCommands
is the main modules to use the API.
-
api.find()
for-find
-
api.findany()
for-findany
-
api.findquery()
for-findquery
-
api.new()
for-new
-
api.edit()
for-edit
-
api.delete()
for-delete
-
api.dup()
for-dup
-
api.view()
for-view
Most API will be smart enough to reject invalid query parameters if passed in incorrectly.
Using Filemaker::Model
If you want ActiveModel-like access with a decent query DSL like where
, find
, in
, you can include Filemaker::Model
to your model. Your Rails form will work as well as JSON serialization.
The following data type mappings can be used to register the fields:
-
string
-Filemaker::Model::Types::Text
-
text
-Filemaker::Model::Types::Text
-
integer
-Filemaker::Model::Types::Integer
-
number
-Filemaker::Model::Types::BigDecimal
-
money
-Filemaker::Model::Types::BigDecimal
-
date
-Filemaker::Model::Types::Date
-
datetime
-Filemaker::Model::Types::Time
-
email
-Filemaker::Model::Types::Email
You can create your own custom type by providing these 3 class methods:
__filemaker_cast_to_ruby_object
__filemaker_serialize_for_update
__filemaker_serialize_for_query
And register it with:
Filemaker::Model::Type.register(:fast_string, FastStringType)
If the field name has spaces, you can use fm_name
to identify the real FileMaker field name.
string :job_id, fm_name: 'JobOrderID', identity: true
You can also use 3 relations: has_many
, belongs_to
and has_portal
.
has_many
will refer to the model's own identity as the reference key while belongs_to
will append _id
for the reference key unless being overridden by reference_key
.
class Job
include Filemaker::Model
database :jobs
layout :job
paginates_per 50
# Taken from filemaker.yml config file, default to :default
# Only use registry if you have multiple FileMaker servers you want to connect
registry :read_slave
string :job_id, fm_name: 'JobOrderID', identity: true
string :title, :requirements
datetime :created_at
datetime :published_at, fm_name: 'ModifiedDate'
money :salary
validates :title, presence: true
belongs_to :company
has_many :applicants, class_name: 'JobApplication', reference_key: 'job_id'
end
# filemaker.yml
development:
default:
host: <%= ENV['FILEMAKER_HOSTNAME'] %>
account_name: <%= ENV['FILEMAKER_ACCOUNT_NAME'] %>
password: <%= ENV['FILEMAKER_PASSWORD'] %>
ssl: true
ssl_verifypeer: false
ssl_verifyhost: 0
log: curl
read_slave:
host: ...
ssl: { verify: false }
production:
default:
host: example.com
ssl: { ca_path: '/secret/path' }
Writing Standalone script
#!/usr/bin/env ruby
require 'bundler/setup'
Bundler.require
Filemaker.registry['default'] = Filemaker::Server.new do |config|
config.host = ENV['FILEMAKER_HOST']
config.account_name = ENV['FILEMAKER_ACCOUNT_NAME']
config.password = ENV['FILEMAKER_PASSWORD']
end
class Invoice
include Filemaker::Model
string :invoice_id, identity: true
date :paid_at
money :amount
end
invoices = Invoice.where(paid_at: '2017')
invoices.each do |invoice|
import_invoice_to_s3
end
Query DSL
Using -find
Model.where(gender: 'male', age: '< 50') # Default -lop=and
Model.where(gender: 'male').or(age: '< 50') # -lop=or
Model.where(gender: 'male').not(age: 40) # age.op=neq
# Supply a block to configure additional options like
# -script, -script.prefind, -lay.response, etc
Model.where(gender: 'male').or(age: '< 50') do |option|
option[:script] = ['RemoveDuplicates', 20]
end
Model.where(gender: 'male').or(name: 'Lee').not(age: '=40')
# DateTime range example
Model.where(timestamp: "10/17/2015 00:00:00...10/20/2015 23:59:59")
# Comparison operator
Model.equals(candidate_id: '123') # { candidate_id: '=123' }
Model.contains(name: 'Chong') # { name: '*Chong*' }
Model.begins_with(salary: '2000...4000') # ??
Model.ends_with(name: 'Yong') # { name: '*Yong' }
Model.gt(age: 20)
Model.gte(age: 20)
Model.lt(age: 20)
Model.lte(age: 20)
Model.not(name: 'Bob')
Using -findquery
OR broadens the found set and AND narrows it
# (q0);(q1)
# (Singapore) OR (Malaysia)
Model.in(nationality: %w[Singapore Malaysia])
# (q0,q1)
# (nationality AND age)
# Essentially the same as:
# Model.where(nationality: 'Singapore', age: 30)
Model.in(nationality: 'Singapore', age: 30)
# (q0);(q1);(q2);(q3)
Model.in({ nationality: %w[Singapore Malaysia] }, { age: [20, 30] })
# (q0,q2);(q1,q2)
# (Singapore AND male) OR (Malaysia AND male)
Model.in(nationality: %w[Singapore Malaysia], gender: 'male')
# !(q0);!(q1)
# NOT(Singapore) OR NOT(Malaysia)
Model.not_in(nationality: %w[Singapore Malaysia])
# !(q0,q1)
Model.not_in(name: 'Lee', age: '< 40')
# !(q0);!(q1)
# Must be within an array of hashes
Model.not_in([{ name: 'Lee' }, { age: '< 40' }])
# (q0);(q1);!(q2,q3)
Model.in(nationality: %w(Singapore Malaysia)).not_in(name: 'Lee', age: '< 40')
Note: It is vitally important that you get the order right for mixing in the use of in
with not_in
. Likely you will want to do an in
first to be inclusive and later omit using not_in
.
- Please test the above query with real data to ensure correctness!
- Please test the comparison operators with keyword as well as applied to value.
- Test serialization of BigDecimal and other types.
- Caching of relation models.
- Dirty checking API for model.
- Test the order for
in
andnot_in
found set accuracy.
Pagination
If you have kaminari in your project's Gemfile
, Filemaker::Model
will use it to page through the returned collection.
Job.where(title: 'admin').per(50) # default to page(1)
Job.where(title: 'admin').page(5) # default to per(25)
Job.where(title: 'admin').page(2).per(35)
# In your model, you can customize the per_page
class Job
include Filemaker::Model
database :jobs
layout :job
paginates_per 50
end
Job.per_page # => 50
Tips
Filemaker::Model
include Filemaker::Model::Findable
which create Criteria
and execute()
to return Filemaker::Resultset
to be built by Filemaker::Model::Builder
.
Credits
This project is heavily inspired by the following Filemaker Ruby effort and several other ORM gems.
Contributing
We welcome pull request with specs.
- Fork it ( https://github.com/mech/filemaker-ruby/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Do run rubocop -D -f simple
before committing.