fm_store 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|