fm_store 0.1
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.
- data/LICENSE +13 -0
- data/README.rdoc +211 -0
- data/lib/fm_store.rb +35 -0
- data/lib/fm_store/associations.rb +44 -0
- data/lib/fm_store/associations/belongs_to.rb +36 -0
- data/lib/fm_store/associations/has_many.rb +39 -0
- data/lib/fm_store/associations/options.rb +30 -0
- data/lib/fm_store/associations/proxy.rb +17 -0
- data/lib/fm_store/builders/collection.rb +33 -0
- data/lib/fm_store/builders/single.rb +25 -0
- data/lib/fm_store/components.rb +19 -0
- data/lib/fm_store/config.rb +19 -0
- data/lib/fm_store/connection.rb +23 -0
- data/lib/fm_store/criteria.rb +98 -0
- data/lib/fm_store/criterion/exclusion.rb +24 -0
- data/lib/fm_store/criterion/inclusion.rb +255 -0
- data/lib/fm_store/criterion/optional.rb +46 -0
- data/lib/fm_store/ext/field.rb +20 -0
- data/lib/fm_store/field.rb +14 -0
- data/lib/fm_store/fields.rb +36 -0
- data/lib/fm_store/finders.rb +36 -0
- data/lib/fm_store/layout.rb +156 -0
- data/lib/fm_store/paging.rb +27 -0
- data/lib/fm_store/persistence.rb +109 -0
- data/lib/fm_store/railtie.rb +17 -0
- data/lib/fm_store/version.rb +4 -0
- data/lib/generators/fm_model/fm_model_generator.rb +28 -0
- data/lib/generators/fm_model/templates/model.rb +10 -0
- metadata +95 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2010 mech
|
2
|
+
|
3
|
+
Permission is hereby granted, to any person obtaining a copy of this software
|
4
|
+
and associated documentation files (the "Software"), including the rights to use,
|
5
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
6
|
+
Software.
|
7
|
+
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
10
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
11
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
12
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
13
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
== FmStore
|
2
|
+
|
3
|
+
Based on Mongoid implementation, FmStore behaves in a way very similar to how
|
4
|
+
you might expect ActiveRecord to behave. With callback and validation and ActiveModel
|
5
|
+
adherence.
|
6
|
+
|
7
|
+
== Database setup
|
8
|
+
|
9
|
+
Place a fm_store.yml file inside the config directory.
|
10
|
+
|
11
|
+
development:
|
12
|
+
host: 127.0.0.1
|
13
|
+
account_name:
|
14
|
+
password:
|
15
|
+
ssl: false
|
16
|
+
log_actions: true
|
17
|
+
|
18
|
+
== Model setup
|
19
|
+
|
20
|
+
You can setup a model using the following generator:
|
21
|
+
|
22
|
+
rails generate fm_model <file_name> <layout_name> <database_name>
|
23
|
+
|
24
|
+
This is using the Job example:
|
25
|
+
|
26
|
+
class Job
|
27
|
+
include FmStore::Layout
|
28
|
+
|
29
|
+
set_layout "jobs"
|
30
|
+
set_database "jobs "
|
31
|
+
|
32
|
+
field :modify_date, DateTime, :fm_name => "modify date"
|
33
|
+
field :company, String
|
34
|
+
field :location, String
|
35
|
+
field :jdid, String
|
36
|
+
... # more fields
|
37
|
+
|
38
|
+
has_many :job_applications, :reference_key => "jdid"
|
39
|
+
|
40
|
+
validates_presence_of :job_title
|
41
|
+
|
42
|
+
# put your class methods here
|
43
|
+
class << self
|
44
|
+
def published
|
45
|
+
where("status" => "open")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
== format_with option
|
51
|
+
|
52
|
+
There will be times when FileMaker give us String or Numeric ID. For example, +caid+ can become "52383.0".
|
53
|
+
What we really want is a String like "52383". You can force it by using the +format_with+ option and provide
|
54
|
+
your own implementation of the ID.
|
55
|
+
|
56
|
+
has_many :employments, :reference_key => "candidate_id", :format_with => :formatted_candidate_id
|
57
|
+
|
58
|
+
def formatted_candidate_id
|
59
|
+
candidate_id.to_s.split(".").first
|
60
|
+
end
|
61
|
+
|
62
|
+
== Finders
|
63
|
+
|
64
|
+
All finder methods such as +where+, +limit+, +order+, +in+ return a criteria object
|
65
|
+
which contain the params and options. No database call will be made until a kicker
|
66
|
+
method has been called such as +each+, +first+, etc.
|
67
|
+
|
68
|
+
# Find single job with ID. There is no #find method. Not to be confused with
|
69
|
+
# the instance method Job#id like @job.id which return you "JDID1" for example.
|
70
|
+
# ID will default to -recid. If you wish to change that, use the :identity option
|
71
|
+
class Job
|
72
|
+
include FmStore::Layout
|
73
|
+
|
74
|
+
field :job_id, String, :fm_name => "jdid", :identity => true
|
75
|
+
end
|
76
|
+
|
77
|
+
@job = Job.id("JDID1") #=> You do not need to append "=" sign to it, like "=JDID1"
|
78
|
+
|
79
|
+
You still can get the original internal FileMaker id by:
|
80
|
+
|
81
|
+
@job = Job.fm_id("18") # Using the -recid
|
82
|
+
|
83
|
+
# Find 10 records
|
84
|
+
@jobs = Job.limit(10)
|
85
|
+
|
86
|
+
# Find 10 records sorted. By default is ASC
|
87
|
+
@jobs = Job.limit(10).order("status") # same as @jobs = Job.limit(10).order("status asc")
|
88
|
+
@jobs = Job.limit(10).order("status desc, jdid")
|
89
|
+
|
90
|
+
# Find based on condition (single value per field). Please use the field name
|
91
|
+
# rather than the FileMaker field name like "modify date"
|
92
|
+
@jobs = Job.where("status" => "open")
|
93
|
+
@jobs = Job.where("category" => "Account", "status" => "open")
|
94
|
+
|
95
|
+
# Find with operator
|
96
|
+
@jobs = Job.where("salary.gt" => 2500)
|
97
|
+
|
98
|
+
# Find all payroll details whose gross salary is between $1.00 to $10.00 in order
|
99
|
+
PayrollDetail.where("gross_salary.bw" => "1...10").order("gross salary")
|
100
|
+
|
101
|
+
# Excluding
|
102
|
+
# WARNING - exclude cannot chain to search for now
|
103
|
+
@jobs = Job.where("status.neq" => "open")
|
104
|
+
@jobs = Job.exclude("status" => "open") # this is preferred
|
105
|
+
|
106
|
+
# Logical OR
|
107
|
+
# By default, conditions are ANDed together. Pass in false to make it ORed together
|
108
|
+
# Remember to supply curly braces for the first parameter which is a hash
|
109
|
+
@jobs = Job.where({"status" => "open", :category => "Account"}, false)
|
110
|
+
|
111
|
+
# Total count
|
112
|
+
@total = Job.where("status" => "closed").total
|
113
|
+
@total = Job.total
|
114
|
+
|
115
|
+
# Find based on multiple values in a single field
|
116
|
+
@jobs = Job.in(:status => ["pending", "closed"])
|
117
|
+
@jobs = Job.in(:status => ["pending", "closed"]).order("status").limit(10)
|
118
|
+
|
119
|
+
# You can also pass in single String instead of an array
|
120
|
+
@jobs = Job.in(:status => "open", :job_id => ["=JD123", "=JD456"]) #=> the value "open" will automatically be ["open"]
|
121
|
+
|
122
|
+
== Search
|
123
|
+
|
124
|
+
Every model will be exposed the +search+ class method. Based on the +searchable+
|
125
|
+
field option, this +search+ method will know which fields to search for.
|
126
|
+
|
127
|
+
# In your model
|
128
|
+
field :name, String, :fm_name => "company", :searchable => true
|
129
|
+
|
130
|
+
# Search for it in your controller
|
131
|
+
@company = Company.search(params)
|
132
|
+
|
133
|
+
@companies = Company.where(:category => "REGULAR").search(params)
|
134
|
+
@jobs = Job.in(:status => ["open", "pending"]).search(:q => "engineer, programmer")
|
135
|
+
|
136
|
+
+search+ will always look for params[:q] as the query keyword and params[:page] as the page number.
|
137
|
+
|
138
|
+
WARNING - Please note that +exclude+ cannot use +search+ for now.
|
139
|
+
|
140
|
+
Some search examples:
|
141
|
+
|
142
|
+
Job.where(:status => "open").search(:q => "engineer") #=> (q0,q1)
|
143
|
+
Job.in(:status => ["open"]).search(:q => "engineer") #=> (q0,q1)
|
144
|
+
Job.in(:status => ["open", "pending"]).search(:q => "engineer") #=> (q0,q2);(q1,q2)
|
145
|
+
|
146
|
+
== Pagination
|
147
|
+
|
148
|
+
Paging is supported via WillPaginate rails3 branch. Any +limit+ criteria will be
|
149
|
+
ignore when you call +paginate+, but you can override +per_page+ as usual.
|
150
|
+
|
151
|
+
@jobs = Job.where("status" => "open").paginate(:page => params[:page] || 1)
|
152
|
+
@jobs = Job.where("status" => "open").paginate(:per_page => 10, :page => params[:page] || 1)
|
153
|
+
|
154
|
+
+paginate+ is a kicker method in itself so database connection will be made and
|
155
|
+
result being retrieved.
|
156
|
+
|
157
|
+
== Custom query
|
158
|
+
|
159
|
+
If any of the +where+, +in+, +exclude+ do not meet your need, you can build the query yourself using FileMaker's +findquery+ command.
|
160
|
+
|
161
|
+
For example:
|
162
|
+
|
163
|
+
JobApplication.custom_query(
|
164
|
+
"-query" => "(q0,q1);!(q2);!(q3);!(q4)",
|
165
|
+
"-q0" => "caid",
|
166
|
+
"-q0.value" => "123",
|
167
|
+
"-q1" => "status2",
|
168
|
+
"-q1.value" => "DeclineOffer",
|
169
|
+
"-q2" => "status1",
|
170
|
+
"-q2.value" => "Apply",
|
171
|
+
"-q3" => "status1",
|
172
|
+
"-q3.value" => "Select",
|
173
|
+
"-q4" => "status1",
|
174
|
+
"-q4.value" => "SMS"
|
175
|
+
).order("date2 DESC")
|
176
|
+
|
177
|
+
The query is
|
178
|
+
|
179
|
+
(q0,q1);!(q2);!(q3);!(q4)
|
180
|
+
|
181
|
+
which means find
|
182
|
+
|
183
|
+
q0 and q1 and omit q2, q3, and q4
|
184
|
+
|
185
|
+
== Available operators
|
186
|
+
|
187
|
+
* eq =word
|
188
|
+
* cn *word*
|
189
|
+
* bw word*
|
190
|
+
* ew *word
|
191
|
+
* gt > word
|
192
|
+
* gte >= word
|
193
|
+
* lt < word
|
194
|
+
* lte <= word
|
195
|
+
* neq omit,word
|
196
|
+
|
197
|
+
== Persistence
|
198
|
+
|
199
|
+
In order to save data to FileMaker, check you have the necessary permission.
|
200
|
+
|
201
|
+
@leave = Leave.new
|
202
|
+
@leave.contract_code = "S1234.01"
|
203
|
+
@leave.fm_attributes #=> {"contract code" => "S1234.01"}
|
204
|
+
@leave.from_date = Date.today
|
205
|
+
@leave.fm_attributes #=> {"from"=>"08/19/2010", "contract code" => "S1234.01"}
|
206
|
+
|
207
|
+
@leave.valid? # test if model is valid or not
|
208
|
+
@leave.errors # get all the errors if there are any
|
209
|
+
@leave.save # this will automatically called valid? and return false if failed
|
210
|
+
|
211
|
+
As you can see, the real work of converting Ruby object to the one suitable for FileMaker is the +fm_attributes+ value method.
|
data/lib/fm_store.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Copyright (c) 2010 mech
|
3
|
+
#
|
4
|
+
require 'rubygems'
|
5
|
+
|
6
|
+
gem "activemodel", ">=3.0.3"
|
7
|
+
gem "will_paginate", "~>3.0.pre"
|
8
|
+
|
9
|
+
require "singleton"
|
10
|
+
require 'yaml'
|
11
|
+
require 'rfm'
|
12
|
+
require 'rfm/metadata/field'
|
13
|
+
require 'fm_store/ext/field'
|
14
|
+
require 'active_support'
|
15
|
+
require 'active_support/core_ext'
|
16
|
+
require 'active_support/notifications'
|
17
|
+
require 'active_model'
|
18
|
+
require 'will_paginate/collection'
|
19
|
+
|
20
|
+
require 'fm_store/config'
|
21
|
+
require 'fm_store/associations'
|
22
|
+
require 'fm_store/persistence'
|
23
|
+
require 'fm_store/builders/collection'
|
24
|
+
require 'fm_store/builders/single'
|
25
|
+
require 'fm_store/connection'
|
26
|
+
require 'fm_store/components'
|
27
|
+
require 'fm_store/field'
|
28
|
+
require 'fm_store/fields'
|
29
|
+
require 'fm_store/finders'
|
30
|
+
require 'fm_store/criteria'
|
31
|
+
require 'fm_store/layout'
|
32
|
+
|
33
|
+
if defined?(Rails)
|
34
|
+
require 'fm_store/railtie'
|
35
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'fm_store/associations/proxy'
|
3
|
+
require 'fm_store/associations/options'
|
4
|
+
require 'fm_store/associations/has_many'
|
5
|
+
require 'fm_store/associations/belongs_to'
|
6
|
+
|
7
|
+
module FmStore
|
8
|
+
module Associations
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def has_many(name, options = {})
|
13
|
+
associate(Associations::HasMany, optionize(name, options), false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def belongs_to(name, options= {})
|
17
|
+
associate(Associations::BelongsTo, optionize(name, options))
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def associate(type, options, cached = true)
|
23
|
+
name = options.name.to_s
|
24
|
+
|
25
|
+
define_method(name) do
|
26
|
+
if cached
|
27
|
+
@associations[name] ||= type.new(self, options)
|
28
|
+
else
|
29
|
+
# do not cached those that returns criteria
|
30
|
+
type.new(self, options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method("#{name}=") do |object|
|
35
|
+
type.update(object, self, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def optionize(name, options)
|
40
|
+
Associations::Options.new(options.merge(:name => name))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Associations
|
4
|
+
class BelongsTo < Proxy
|
5
|
+
|
6
|
+
attr_accessor :association_name, :klass
|
7
|
+
|
8
|
+
def initialize(layout, options)
|
9
|
+
@layout, @association_name = layout, options.name
|
10
|
+
@klass, @reference_key, @options = options.klass, options.reference_key, options
|
11
|
+
@format_with = options.format_with
|
12
|
+
|
13
|
+
build_parent
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def build_parent
|
19
|
+
if @format_with
|
20
|
+
if @layout.send(@format_with.to_sym).nil?
|
21
|
+
@target = nil
|
22
|
+
else
|
23
|
+
@target = @klass.where({@reference_key => "=#{@layout.send(@format_with.to_sym)}"}).limit(1).first
|
24
|
+
end
|
25
|
+
else
|
26
|
+
if @layout.send(@reference_key.to_sym).nil?
|
27
|
+
@target = nil
|
28
|
+
else
|
29
|
+
@target = @klass.where({@reference_key => "=#{@layout.send(@reference_key.to_sym)}"}).limit(1).first
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Associations
|
4
|
+
class HasMany < Proxy
|
5
|
+
|
6
|
+
attr_accessor :association_name, :klass
|
7
|
+
|
8
|
+
# @layout is an instance of the model and not the class itself
|
9
|
+
def initialize(layout, options)
|
10
|
+
@layout, @association_name = layout, options.name
|
11
|
+
@klass, @reference_key, @options = options.klass, options.reference_key, options
|
12
|
+
@format_with = options.format_with
|
13
|
+
|
14
|
+
build_children
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def build_children
|
20
|
+
# Returns a criteria rather then grabbing the records, so we do not
|
21
|
+
# waste request trip
|
22
|
+
if @format_with
|
23
|
+
if @layout.send(@format_with.to_sym).nil?
|
24
|
+
@target = nil
|
25
|
+
else
|
26
|
+
@target = @klass.where({@reference_key => "=#{@layout.send(@format_with.to_sym)}"})
|
27
|
+
end
|
28
|
+
else
|
29
|
+
if @layout.send(@reference_key.to_sym).nil?
|
30
|
+
@target = nil
|
31
|
+
else
|
32
|
+
@target = @klass.where({@reference_key => "=#{@layout.send(@reference_key.to_sym)}"})
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Associations
|
4
|
+
class Options
|
5
|
+
def initialize(attributes = {})
|
6
|
+
@attributes = attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
def reference_key
|
10
|
+
@attributes[:reference_key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def klass
|
14
|
+
class_name.constantize
|
15
|
+
end
|
16
|
+
|
17
|
+
def class_name
|
18
|
+
@attributes[:class_name] || name.to_s.classify
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
@attributes[:name].to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def format_with
|
26
|
+
@attributes[:format_with]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore #:nodoc
|
3
|
+
module Associations
|
4
|
+
class Proxy
|
5
|
+
instance_methods.each do |method|
|
6
|
+
undef_method(method) unless method =~ /(^__|^nil\?$|^send$|^object_id$|^extend$)/
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :target, :options
|
10
|
+
|
11
|
+
def method_missing(name, *args, &block)
|
12
|
+
@target.send(name, *args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Builders
|
4
|
+
class Collection
|
5
|
+
# Build an array of native object (i.e. Job, Payroll, etc) from the
|
6
|
+
# FileMaker records.
|
7
|
+
#
|
8
|
+
# Pass in the desired <tt>model</tt>
|
9
|
+
def self.build(records, model)
|
10
|
+
target = []
|
11
|
+
|
12
|
+
records.each do |record|
|
13
|
+
fm_fields = record.keys
|
14
|
+
|
15
|
+
obj = model.new
|
16
|
+
|
17
|
+
fm_fields.each do |fm_field|
|
18
|
+
field = model.fields[fm_field] # Field
|
19
|
+
obj.instance_variable_set("@#{field.name}", record[fm_field]) if field.respond_to?(:name)
|
20
|
+
end
|
21
|
+
|
22
|
+
obj.instance_variable_set("@new_record", false)
|
23
|
+
obj.instance_variable_set("@mod_id", record.mod_id)
|
24
|
+
obj.instance_variable_set("@record_id", record.record_id)
|
25
|
+
|
26
|
+
target << obj
|
27
|
+
end
|
28
|
+
|
29
|
+
return target
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|