filemaker 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +129 -17
- data/filemaker.gemspec +1 -0
- data/lib/filemaker.rb +47 -0
- data/lib/filemaker/api/query_commands/findquery.rb +20 -3
- data/lib/filemaker/configuration.rb +1 -1
- data/lib/filemaker/core_ext/hash.rb +19 -15
- data/lib/filemaker/error.rb +5 -0
- data/lib/filemaker/metadata/field.rb +21 -5
- data/lib/filemaker/model.rb +132 -0
- data/lib/filemaker/model/builder.rb +52 -0
- data/lib/filemaker/model/components.rb +25 -0
- data/lib/filemaker/model/criteria.rb +101 -0
- data/lib/filemaker/model/field.rb +38 -0
- data/lib/filemaker/model/fields.rb +80 -0
- data/lib/filemaker/model/findable.rb +35 -0
- data/lib/filemaker/model/optional.rb +69 -0
- data/lib/filemaker/model/pagination.rb +41 -0
- data/lib/filemaker/model/persistable.rb +96 -0
- data/lib/filemaker/model/relations.rb +72 -0
- data/lib/filemaker/model/relations/belongs_to.rb +30 -0
- data/lib/filemaker/model/relations/has_many.rb +79 -0
- data/lib/filemaker/model/relations/proxy.rb +35 -0
- data/lib/filemaker/model/selectable.rb +146 -0
- data/lib/filemaker/railtie.rb +17 -0
- data/lib/filemaker/record.rb +25 -0
- data/lib/filemaker/resultset.rb +12 -4
- data/lib/filemaker/server.rb +7 -5
- data/lib/filemaker/version.rb +1 -1
- data/spec/filemaker/api/query_commands/compound_find_spec.rb +13 -1
- data/spec/filemaker/configuration_spec.rb +23 -0
- data/spec/filemaker/layout_spec.rb +0 -1
- data/spec/filemaker/model/criteria_spec.rb +304 -0
- data/spec/filemaker/model/relations_spec.rb +85 -0
- data/spec/filemaker/model_spec.rb +73 -0
- data/spec/filemaker/record_spec.rb +12 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/filemaker.yml +13 -0
- data/spec/support/models.rb +38 -0
- metadata +44 -2
@@ -0,0 +1,96 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Persistable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
define_model_callbacks :save, :create, :update, :destroy
|
8
|
+
end
|
9
|
+
|
10
|
+
# Call save! but do not raise error.
|
11
|
+
def save
|
12
|
+
save!
|
13
|
+
rescue
|
14
|
+
errors.add(:base) << $! # Does this works?
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def save!
|
19
|
+
run_callbacks :save do
|
20
|
+
new_record? ? create : update
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def create
|
25
|
+
return false unless valid?
|
26
|
+
|
27
|
+
run_callbacks :create do
|
28
|
+
options = {}
|
29
|
+
yield options if block_given?
|
30
|
+
resultset = api.new(fm_attributes, options)
|
31
|
+
replace_new_data(resultset)
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def update
|
37
|
+
return false unless valid?
|
38
|
+
|
39
|
+
run_callbacks :update do
|
40
|
+
# Will raise `RecordModificationIdMismatchError` if does not match
|
41
|
+
options = { modid: mod_id } # Always pass in?
|
42
|
+
yield options if block_given?
|
43
|
+
resultset = api.edit(record_id, fm_attributes, options)
|
44
|
+
replace_new_data(resultset)
|
45
|
+
end
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_attributes(attrs = {})
|
50
|
+
return self if attrs.blank?
|
51
|
+
assign_attributes(attrs)
|
52
|
+
save
|
53
|
+
end
|
54
|
+
|
55
|
+
# Use -delete to remove the record backed by the model.
|
56
|
+
# @return [Filemaker::Model] frozen instance
|
57
|
+
def destroy
|
58
|
+
return if new_record?
|
59
|
+
|
60
|
+
run_callbacks :destroy do
|
61
|
+
options = {}
|
62
|
+
yield options if block_given?
|
63
|
+
api.delete(record_id, options)
|
64
|
+
end
|
65
|
+
freeze
|
66
|
+
end
|
67
|
+
alias_method :delete, :destroy
|
68
|
+
|
69
|
+
def assign_attributes(new_attributes)
|
70
|
+
return if new_attributes.blank?
|
71
|
+
new_attributes.each_pair do |key, value|
|
72
|
+
public_send("#{key}=", value) if respond_to?("#{key}=")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# If you have calculated field from FileMaker, it will be replaced.
|
79
|
+
def replace_new_data(resultset)
|
80
|
+
record = resultset.first
|
81
|
+
|
82
|
+
@new_record = false
|
83
|
+
@record_id = record.record_id
|
84
|
+
@mod_id = record.mod_id
|
85
|
+
|
86
|
+
record.keys.each do |fm_field_name|
|
87
|
+
# record.keys are all lowercase
|
88
|
+
field = self.class.find_field_by_name(fm_field_name)
|
89
|
+
next unless field
|
90
|
+
|
91
|
+
public_send("#{field.name}=", record[fm_field_name])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'filemaker/model/relations/belongs_to'
|
2
|
+
require 'filemaker/model/relations/has_many'
|
3
|
+
|
4
|
+
module Filemaker
|
5
|
+
module Model
|
6
|
+
# Model relationships such as has_many, belongs_to, and has_portal.
|
7
|
+
module Relations
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
attr_reader :relations
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def has_many(name, options = {})
|
16
|
+
relate_collection(Relations::HasMany, name, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def belongs_to(name, options = {})
|
20
|
+
relate_single(Relations::BelongsTo, name, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_portal(name, options = {})
|
24
|
+
Relations::HasPortal.new(self, name, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
# Get the single model and cache it to `relations`
|
30
|
+
def relate_single(type, name, options)
|
31
|
+
name = name.to_s
|
32
|
+
|
33
|
+
# Reader
|
34
|
+
#
|
35
|
+
# @example Reload the record
|
36
|
+
# job.company(true)
|
37
|
+
define_method(name) do |force_reload = false|
|
38
|
+
if force_reload
|
39
|
+
@relations[name] = type.new(self, name, options)
|
40
|
+
else
|
41
|
+
@relations[name] ||= type.new(self, name, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Writer
|
46
|
+
#
|
47
|
+
# TODO: What happen if `object` is brand new? We would want to save
|
48
|
+
# the child as well as the parent. We need to wait for the child to
|
49
|
+
# save and return the identity ID, then we update the parent's
|
50
|
+
# reference_key.
|
51
|
+
define_method("#{name}=") do |object|
|
52
|
+
params = { "#{name}_id" => object.public_send("#{name}_id") }
|
53
|
+
update_attributes(params)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Creator
|
57
|
+
# define_method("create_#{name}") do |attrs = {}|
|
58
|
+
# end
|
59
|
+
end
|
60
|
+
|
61
|
+
# For collection, we will return criteria and not cache anything.
|
62
|
+
def relate_collection(type, name, options)
|
63
|
+
name = name.to_s
|
64
|
+
|
65
|
+
define_method(name) do
|
66
|
+
type.new(self, name, options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'filemaker/model/relations/proxy'
|
2
|
+
|
3
|
+
module Filemaker
|
4
|
+
module Model
|
5
|
+
module Relations
|
6
|
+
class BelongsTo < Proxy
|
7
|
+
def initialize(owner, name, options)
|
8
|
+
super(owner, name, options)
|
9
|
+
build_target
|
10
|
+
end
|
11
|
+
|
12
|
+
def reference_key
|
13
|
+
options.fetch(:reference_key) { "#{@name}_id" }
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def build_target
|
19
|
+
key_value = owner.public_send(reference_key.to_sym)
|
20
|
+
|
21
|
+
if key_value.blank?
|
22
|
+
@target = nil
|
23
|
+
else
|
24
|
+
@target = target_class.where(reference_key => "=#{key_value}").first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'filemaker/model/relations/proxy'
|
2
|
+
|
3
|
+
module Filemaker
|
4
|
+
module Model
|
5
|
+
module Relations
|
6
|
+
class HasMany < Proxy
|
7
|
+
def initialize(owner, name, options)
|
8
|
+
super(owner, name, options)
|
9
|
+
build_target
|
10
|
+
end
|
11
|
+
|
12
|
+
# If no reference_key, we will use owner's identity field. If there is
|
13
|
+
# no identity, we will...??
|
14
|
+
def reference_key
|
15
|
+
options.fetch(:reference_key) { owner.identity.name }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Append a model or array of models to the relation. Will set the owner
|
19
|
+
# ID to the children.
|
20
|
+
#
|
21
|
+
# @example Append a model
|
22
|
+
# job.applicants << applicant
|
23
|
+
#
|
24
|
+
# @example Array of models
|
25
|
+
# job.applicants << [applicant_a, applicant_b, applicant_c]
|
26
|
+
#
|
27
|
+
# @param [Filemaker::Model, Array<Filemaker::Model>] *args
|
28
|
+
def <<(*args)
|
29
|
+
docs = args.flatten
|
30
|
+
return concat(docs) if docs.size > 1
|
31
|
+
if (doc = docs.first)
|
32
|
+
create(doc)
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
alias_method :push, :<<
|
37
|
+
|
38
|
+
# def concat(docs)
|
39
|
+
# # TODO: Find out how to do batch insert in FileMaker
|
40
|
+
# end
|
41
|
+
|
42
|
+
# Build a single model. The owner will be linked, but the record will
|
43
|
+
# not be saved.
|
44
|
+
#
|
45
|
+
# @example Append a model
|
46
|
+
# job.applicants.build(name: 'Bob')
|
47
|
+
# job.save
|
48
|
+
#
|
49
|
+
# @param [Hash] attrs The attributes for the fields
|
50
|
+
#
|
51
|
+
# @return [Filemaker::Model] the actual model
|
52
|
+
def build(attrs = {})
|
53
|
+
attrs.merge!(owner.identity.name => owner.identity_id) if \
|
54
|
+
owner.identity_id
|
55
|
+
target_class.new(attrs)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Same as `build`, except that it will be saved automatically.
|
59
|
+
#
|
60
|
+
# @return [Filemaker::Model] the actual saved model
|
61
|
+
def create(attrs = {})
|
62
|
+
build(attrs).save
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def build_target
|
68
|
+
key_value = owner.public_send(reference_key.to_sym)
|
69
|
+
|
70
|
+
if key_value.blank?
|
71
|
+
@target = nil # Or should we return empty array?
|
72
|
+
else
|
73
|
+
@target = target_class.where(reference_key => "=#{key_value}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Relations
|
4
|
+
# A proxy is a class to send all unknown methods to it's target. The
|
5
|
+
# target here will be the eventual associated model.
|
6
|
+
class Proxy
|
7
|
+
instance_methods.each do |method|
|
8
|
+
undef_method(method) unless
|
9
|
+
method =~ /(^__|^send|^object_id|^respond_to|^tap|^extend)/
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :owner, :target, :options
|
13
|
+
|
14
|
+
# @param [Filemaker::Layout] owner The instance of the model
|
15
|
+
# @param [String] name The relationship name
|
16
|
+
# @param [Hash] options Relationship options
|
17
|
+
def initialize(owner, name, options)
|
18
|
+
@owner = owner
|
19
|
+
@name = name
|
20
|
+
@options = options
|
21
|
+
@class_name = options.fetch(:class_name) { name.to_s.classify }
|
22
|
+
end
|
23
|
+
|
24
|
+
def target_class
|
25
|
+
return @class_name if @class_name.is_a?(Class)
|
26
|
+
@class_name.constantize
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(name, *args, &block)
|
30
|
+
target.send(name, *args, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Filemaker
|
2
|
+
module Model
|
3
|
+
module Selectable
|
4
|
+
# Find records based on query hash.
|
5
|
+
#
|
6
|
+
# @param [Hash] criterion Hash criterion
|
7
|
+
#
|
8
|
+
# @return [Filemaker::Model::Criteria]
|
9
|
+
def where(criterion)
|
10
|
+
fail Filemaker::Error::MixedClauseError,
|
11
|
+
"Can't mix 'where' with 'in'." if chains.include?(:in)
|
12
|
+
chains.push(:where)
|
13
|
+
|
14
|
+
@selector ||= {}
|
15
|
+
selector.merge!(klass.with_model_fields(criterion))
|
16
|
+
yield options if block_given?
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# Find records based on model ID. If passed a hash, will use `where`.
|
21
|
+
# On the last resort, if we seriously can't find using `where`, we find
|
22
|
+
# it thru the `recid`. Is this a good design? We will see in production.
|
23
|
+
# Performance note: 2 HTTP requests if going that last resort route.
|
24
|
+
#
|
25
|
+
# @example Find by model ID.
|
26
|
+
# Model.find('CAID324')
|
27
|
+
#
|
28
|
+
# @example Find with a Hash. This will delegate to `where`.
|
29
|
+
# Model.find(name: 'Bob', salary: 4000)
|
30
|
+
#
|
31
|
+
# @param [Integer, String, Hash] criterion
|
32
|
+
#
|
33
|
+
# @return [Filemaker::Model::Criteria, Filemaker::Model]
|
34
|
+
def find(criterion)
|
35
|
+
return where(criterion) if criterion.is_a? Hash
|
36
|
+
|
37
|
+
# Find using model ID (may not be the -recid)
|
38
|
+
id = criterion.to_s.gsub(/\A=*/, '=') # Always append '=' for ID
|
39
|
+
|
40
|
+
# If we are finding with ID, we just limit to one and return
|
41
|
+
# immediately. Last resort is to use the recid to find.
|
42
|
+
where(klass.identity.name => id).first || recid(criterion)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Using FileMaker's internal ID to find the record.
|
46
|
+
def recid(id)
|
47
|
+
return nil if id.blank?
|
48
|
+
|
49
|
+
@selector ||= {}
|
50
|
+
selector['-recid'] = id
|
51
|
+
chains.push(:where)
|
52
|
+
first
|
53
|
+
end
|
54
|
+
|
55
|
+
%w(eq cn bw ew gt gte lt lte neq).each do |operator|
|
56
|
+
define_method(operator) do |criterion, &block|
|
57
|
+
fail Filemaker::Error::MixedClauseError,
|
58
|
+
"Can't mix 'where' with 'in'." if chains.include?(:in)
|
59
|
+
chains.push(operator.to_sym)
|
60
|
+
chains.push(:where) unless chains.include?(:where) # Just one time
|
61
|
+
@selector ||= {}
|
62
|
+
|
63
|
+
if operator == 'bw'
|
64
|
+
criterion = klass.with_model_fields(criterion, false)
|
65
|
+
else
|
66
|
+
criterion = klass.with_model_fields(criterion)
|
67
|
+
end
|
68
|
+
|
69
|
+
criterion.each_key do |key|
|
70
|
+
selector["#{key}.op"] = operator
|
71
|
+
end
|
72
|
+
|
73
|
+
selector.merge!(criterion)
|
74
|
+
|
75
|
+
# Inside define_method, we cannot have yield or block_given?, so we
|
76
|
+
# just use &block
|
77
|
+
block.call(options) if block
|
78
|
+
self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
alias_method :equals, :eq
|
83
|
+
alias_method :contains, :cn
|
84
|
+
alias_method :begins_with, :bw
|
85
|
+
alias_method :ends_with, :ew
|
86
|
+
alias_method :not, :neq
|
87
|
+
|
88
|
+
# Find records based on FileMaker's compound find syntax.
|
89
|
+
#
|
90
|
+
# @example Find using a single hash
|
91
|
+
# Model.in(nationality: %w(Singapore Malaysia))
|
92
|
+
#
|
93
|
+
# @example Find using an array of hashes
|
94
|
+
# Model.in([{nationality: %w(Singapore Malaysia)}, {age: [20, 30]}])
|
95
|
+
#
|
96
|
+
# @param [Hash, Array]
|
97
|
+
#
|
98
|
+
# @return [Filemaker::Model::Criteria]
|
99
|
+
def in(criterion, negating = false)
|
100
|
+
fail Filemaker::Error::MixedClauseError,
|
101
|
+
"Can't mix 'in' with 'where'." if chains.include?(:where)
|
102
|
+
chains.push(:in)
|
103
|
+
@selector ||= []
|
104
|
+
|
105
|
+
become_array(criterion).each do |hash|
|
106
|
+
accepted_hash = klass.with_model_fields(hash)
|
107
|
+
accepted_hash.merge!('-omit' => true) if negating
|
108
|
+
@selector << accepted_hash
|
109
|
+
end
|
110
|
+
|
111
|
+
yield options if block_given?
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Simply append '-omit' => true to all criteria
|
116
|
+
def not_in(criterion)
|
117
|
+
self.in(criterion, true)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Used with `where` to specify how the queries are combined. Default is
|
121
|
+
# 'and', so you won't find any `and` method.
|
122
|
+
#
|
123
|
+
# @example Mix with where to 'or' query
|
124
|
+
# Model.where(name: 'Bob').or(age: '50')
|
125
|
+
#
|
126
|
+
# @param [Hash] criterion Hash criterion
|
127
|
+
#
|
128
|
+
# @return [Filemaker::Model::Criteria]
|
129
|
+
def or(criterion)
|
130
|
+
fail Filemaker::Error::MixedClauseError,
|
131
|
+
"Can't mix 'or' with 'in'." if chains.include?(:in)
|
132
|
+
@selector ||= {}
|
133
|
+
selector.merge!(klass.with_model_fields(criterion))
|
134
|
+
options[:lop] = 'or'
|
135
|
+
yield options if block_given?
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def become_array(value)
|
142
|
+
value.is_a?(Array) ? value : [value]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|