Project

filemaker

0.02
No release in over 3 years
Low commit activity in last 3 years
Provides ActiveModel-like object to read and write.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
~> 3.7

Runtime

 Project Readme

Filemaker

Build Status security

A Ruby wrapper to FileMaker XML API.

UML - just kidding

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 use ENV variable like ENV['FILEMAKER_ACCOUNT']
  • password - Please use ENV variable like ENV['FILEMAKER_PASSWORD']
  • ssl - Use { verify: false } if you are using FileMaker's unsigned certificate.
  • ssl_verifypeer - Default to false
  • ssl_verifyhost - Default to 0
  • log - A choice of simple, curl and curl_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 and not_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.

  1. Fork it ( https://github.com/mech/filemaker-ruby/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Do run rubocop -D -f simple before committing.