filemaker 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +129 -17
  3. data/filemaker.gemspec +1 -0
  4. data/lib/filemaker.rb +47 -0
  5. data/lib/filemaker/api/query_commands/findquery.rb +20 -3
  6. data/lib/filemaker/configuration.rb +1 -1
  7. data/lib/filemaker/core_ext/hash.rb +19 -15
  8. data/lib/filemaker/error.rb +5 -0
  9. data/lib/filemaker/metadata/field.rb +21 -5
  10. data/lib/filemaker/model.rb +132 -0
  11. data/lib/filemaker/model/builder.rb +52 -0
  12. data/lib/filemaker/model/components.rb +25 -0
  13. data/lib/filemaker/model/criteria.rb +101 -0
  14. data/lib/filemaker/model/field.rb +38 -0
  15. data/lib/filemaker/model/fields.rb +80 -0
  16. data/lib/filemaker/model/findable.rb +35 -0
  17. data/lib/filemaker/model/optional.rb +69 -0
  18. data/lib/filemaker/model/pagination.rb +41 -0
  19. data/lib/filemaker/model/persistable.rb +96 -0
  20. data/lib/filemaker/model/relations.rb +72 -0
  21. data/lib/filemaker/model/relations/belongs_to.rb +30 -0
  22. data/lib/filemaker/model/relations/has_many.rb +79 -0
  23. data/lib/filemaker/model/relations/proxy.rb +35 -0
  24. data/lib/filemaker/model/selectable.rb +146 -0
  25. data/lib/filemaker/railtie.rb +17 -0
  26. data/lib/filemaker/record.rb +25 -0
  27. data/lib/filemaker/resultset.rb +12 -4
  28. data/lib/filemaker/server.rb +7 -5
  29. data/lib/filemaker/version.rb +1 -1
  30. data/spec/filemaker/api/query_commands/compound_find_spec.rb +13 -1
  31. data/spec/filemaker/configuration_spec.rb +23 -0
  32. data/spec/filemaker/layout_spec.rb +0 -1
  33. data/spec/filemaker/model/criteria_spec.rb +304 -0
  34. data/spec/filemaker/model/relations_spec.rb +85 -0
  35. data/spec/filemaker/model_spec.rb +73 -0
  36. data/spec/filemaker/record_spec.rb +12 -1
  37. data/spec/spec_helper.rb +1 -0
  38. data/spec/support/filemaker.yml +13 -0
  39. data/spec/support/models.rb +38 -0
  40. 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