dm-filemaker-adapter 0.0.3 → 0.0.4

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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZjdlOGM0ZWYwNzU2MzhjZGQ3Y2U3NWZkMDY4ZDdlZTQ4NGMwOTc5MA==
4
+ NDE2YzRlM2E5YjU3NzFjYjAyZjM2NGVhMzE4YjExM2U4MjVmOGI2NA==
5
5
  data.tar.gz: !binary |-
6
- NmVhZmNlOWYxNGJmOWNhNDA2NmZjODRlNTZjMmI1MTRkYTZkYmU4OA==
6
+ ZTkzMmU3MmM2MmVlNmVkYmI2N2RiOTUwYzc5MDNlZWIyZTY5N2VlNg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- YWI5ZGRmN2FjNWRhMmY1ZTcwMzBmNmJhYjllNjlhZWE1NDFkZWNlYmFlY2Iw
10
- OGZlMDMzZjYwZmRiMTVmNjY1MTVjMjhlYzlkNjYzZTUwMGM2OWYwNDVmYmU0
11
- ZmNkNDI2NWNkN2Y5MDE0MjEzMWUwMTBlNTg2OGY3NmRjYWVjMTU=
9
+ ZmMzZDIyNTgyZTZlYmQwZGU3M2U2YTg2MzcxMGFhMzRkODM0MzhiZDRhMjEz
10
+ NzZkYmExZjEzNTAwNmE2ZDk4Y2YwMzNjZWMyYTJhMzAzNjg0NTljOTM0YTYz
11
+ ZDgzMjczZWExYTk4NmFmNTlkYzZlMjZmMmE4MDU5NTMzZWNkYjk=
12
12
  data.tar.gz: !binary |-
13
- MWYwMGEzMGZkMzQ3NmQ2NmNiYTE5YWY0ZGQ1YmEwNzJhODEyMmVjYTExZWVl
14
- N2MyZmE1N2VhOTg3NjhmOTE4MDVhMWRmZDZlNmEzODkyYzA4NGYxZGI4NmYx
15
- OTYyODIwNzc0NmNiMDc4YzMxZjY5MDJiYmJhMjI1NTQ3ODNlYjE=
13
+ MTg2MDViODIyNTU4NzRiZGUxOTkzZWRlYjFkODcxYjhlMzVlYzZjMGU4NzJk
14
+ N2E4NWYxYWQ2NmRiMjQ4N2ZlMjlhNmNkOWM2ZTFiNDI5NjkyN2ViZWU5OTg5
15
+ Y2NmY2JiODY3NWM0ZmI4NDFkYjRlMWUxMTBjM2JjM2ExYTkzNzA=
data/Gemfile CHANGED
@@ -2,5 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
-
6
-
5
+ # Only enable this if you are testing against a local copy of ginjo-rfm.
6
+ gem 'ginjo-rfm', :path=>'../GinjoRfm.git/'
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Filemaker adapter for DataMapper, allowing DataMapper to use Filemaker Server as a datastore.
4
4
 
5
- dm-filemaker-adapter uses the ginjo-rfm gem as the backend command and xml parser. Ginjo-rfm is a full featured filemaker-ruby adapter that exposes most of Filemaker's xml interface functionality in ruby. dm-filemaker-adapter doesn't tap into all of rfm's features, but rather, dm-filemaker-adapter provides DataMapper the ability to use Filemaker Server as a backend datastore. All of the basic functionality of DataMapper's CRUD interface is supported, including compound queries and 'or' queries (using Filemaker's -findquery command), query operators like :field.gt=>..., lazy-loading where possible, first & last record, aggregate queries, ranges, field mapping, and more.
5
+ dm-filemaker-adapter uses the ginjo-rfm gem as the backend command and xml parser. Ginjo-rfm is a full featured filemaker-ruby adapter that exposes most of Filemaker's xml interface functionality in ruby. dm-filemaker-adapter doesn't tap into all of rfm's features, but rather, it provides DataMapper the ability to use Filemaker Server as a backend datastore. Most of the basic functionality of DataMapper's interface is supported, including compound queries and 'or' queries (using Filemaker's -findquery command), validations, associations, query operators like ```:field.gt=>something```, lazy-loading where possible, first & last record, aggregate queries, ranges, field mapping, and more.
6
6
 
7
7
  ## Installation
8
8
 
@@ -34,24 +34,41 @@ Or install it yourself as:
34
34
 
35
35
  class User
36
36
  include DataMapper::Resource
37
- storage_names[:default] = 'user_xml' # This is the name of a filemaker layout representing the table you're modeling.
38
37
 
39
- # Property & field names in this list must be lowercase, regardless of what they are in Filemaker.
38
+ # Name the filemaker layout representing the table you're modeling.
39
+ storage_names[:default] = 'user_xml'
40
40
 
41
+ # Property & field names are case-sensitive.
41
42
  property :id, Serial
42
43
  property :username, String, :length => 128, :unique => true, :required => true,
43
44
  :default => lambda {|r,v| r.instance_variable_get :@email}
44
45
  property :email, String, :length => 128, :unique => true, :required => true, :format=>:email_address
45
- property :updated_at, DateTime, :field=>'modification_timestamp'
46
+ property :updated_at, DateTime, :field=>'ModifiedAtTimestamp'
46
47
  property :encrypted_password, BCryptPassword
48
+
49
+ has n, :orders
50
+ end
51
+
52
+ class Order
53
+ include DataMapper::Resource
54
+ storage_names[:default] = 'order_xml'
55
+
56
+ property :id, Serial
57
+ property :user_id, Integer
58
+ property :status, String
59
+
60
+ belongs_to :user
47
61
  end
48
62
 
49
63
  DataMapper.finalize
50
64
 
51
65
 
52
66
 
53
- # create records
54
- User.create(:email => 'abc@company.com', :username => 'abc')
67
+ # create record
68
+ user = User.create(:email => 'abc@company.com', :username => 'abc')
69
+
70
+ # create associated record
71
+ user.orders.new(:status=>'draft')
55
72
 
56
73
  # get a specific user id
57
74
  User.get '1035'
@@ -65,15 +82,18 @@ Or install it yourself as:
65
82
  # records 10 thru 20, ordered by :id (the range is resolved by filemaker, before records are returned!)
66
83
  User.all(:order => :id)[10..20]
67
84
 
85
+ # filter associated records
86
+ user.orders.all(:status=>'closed')
87
+
68
88
  # use the union operator to create 2 find requests in a filemaker 'OR' operation
69
89
  User.all(:email => 'abc@company.com', :activated_at.gt => '1/1/1980') | \
70
90
  User.all(:username => 'abc', :activated_at.gt => '1/1/1980')
71
91
 
72
92
  # which gets translated to the filemaker query
73
- User.find [
74
- {:email => 'abc@company.com', :activated_at => '>1/1/1980'},
75
- {:username => 'abc', :activated_at.gt => '>1/1/1980'}
76
- ]
93
+ User.find [
94
+ {:email => 'abc@company.com', :activated_at => '>1/1/1980'},
95
+ {:username => 'abc', :activated_at.gt => '>1/1/1980'}
96
+ ]
77
97
 
78
98
  # use the intersection operator to combine multiple search criteria in a filemaker 'AND' operation
79
99
  User.all(:email => 'abc@company.com', :activated_at.gt => '1/1/2015') & \
@@ -83,7 +103,7 @@ Or install it yourself as:
83
103
  User.all(:email => 'abc@company.com', :activated_at.gt => '1/1/2015', :activated_at.lt => '5/1/2015')
84
104
 
85
105
  # both of the above get translated to the filemaker query
86
- User.find(:email => 'abc@company.com', :activated_at => '>1/1/2015 <5/1/2015')
106
+ User.find(:email => 'abc@company.com', :activated_at => '>1/1/2015 <5/1/2015')
87
107
 
88
108
 
89
109
 
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'bundler'
2
2
  Bundler.require
3
-
4
3
  require "bundler/gem_tasks"
4
+
5
5
  require "rspec/core/rake_task"
6
6
 
7
7
  RSpec::Core::RakeTask.new(:spec)
@@ -5,9 +5,8 @@ module DataMapper
5
5
  module Adapters
6
6
 
7
7
  class FilemakerAdapter < AbstractAdapter
8
- @fmresultset_template_path = File.expand_path('../dm-fmresultset.yml', __FILE__).to_s
9
- class << self; attr_accessor :fmresultset_template_path; end
10
8
  VERSION = DataMapper::FilemakerAdapter::VERSION
9
+ FMRESULTSET_TEMPLATE = {:template => File.expand_path('../dm-fmresultset.yml', __FILE__)}
11
10
 
12
11
 
13
12
  ### ADAPTER CORE METHODS ###
@@ -27,11 +26,11 @@ module DataMapper
27
26
  #
28
27
  # @api semipublic
29
28
  def create(resources)
30
- resources[0].model.last_query = resources
29
+ #resources[0].model.last_query = resources
31
30
  counter = 0
32
31
  resources.each do |resource|
33
32
  fm_params = prepare_fmp_attributes(resource.dirty_attributes)
34
- rslt = layout(resource.model).create(fm_params, :template=>self.class.fmresultset_template_path)
33
+ rslt = layout(resource.model).create(fm_params)
35
34
  merge_fmp_response(resource, rslt[0])
36
35
  counter +=1
37
36
  end
@@ -57,27 +56,29 @@ module DataMapper
57
56
  # end
58
57
  #
59
58
  def read(query)
60
- query.model.last_query = query
61
- #y query
59
+ #query.model.last_query = query
60
+ #puts query.to_yaml
62
61
  _layout = layout(query.model)
63
62
  opts = query.fmp_options
64
- #puts "FMP OPTIONS #{opts.inspect}"
65
- opts[:template] = self.class.fmresultset_template_path
63
+ #puts "ADAPTER#READ fmp options: #{opts.inspect}"
64
+ #opts[:template] = self.class.fmresultset_template_path
66
65
  prms = query.to_fmp_query
67
- #puts "ADAPTER#read fmp_query built: #{prms.inspect}"
66
+ #puts "ADAPTER#READ fmpquery: #{prms.inspect}"
67
+ #puts "ADAPTER#READ layout config: #{_layout.get_config.inspect}"
68
68
  rslt = prms.empty? ? _layout.all(opts) : _layout.find(prms, opts)
69
- rslt.dup.each_with_index(){|r, i| rslt[i] = r.to_h}
70
- rslt
69
+ # This was here to make rfm records loadable by dm, but no longer needed with Rfm#record @loaded disabled in parsing template.
70
+ #rslt.collect{|r| Hash.new.merge(r) }
71
+ #rslt.to_a
71
72
  end
72
73
 
73
74
  # Takes a query and returns number of matched records.
74
75
  # An empty query will return the total record count
75
76
  def aggregate(query)
76
- query.model.last_query = query
77
+ #query.model.last_query = query
77
78
  #y query
78
79
  _layout = layout(query.model)
79
80
  opts = query.fmp_options
80
- opts[:template] = self.class.fmresultset_template_path
81
+ #opts[:template] = self.class.fmresultset_template_path
81
82
  prms = query.to_fmp_query
82
83
  #[prms.empty? ? _layout.all(:max_records=>0).foundset_count : _layout.count(prms)]
83
84
  [prms.empty? ? _layout.view.total_count : _layout.count(prms)]
@@ -100,11 +101,11 @@ module DataMapper
100
101
  #
101
102
  # @api semipublic
102
103
  def update(attributes, collection)
103
- collection[0].model.last_query = [attributes, collection]
104
+ #collection[0].model.last_query = [attributes, collection]
104
105
  fm_params = prepare_fmp_attributes(attributes)
105
106
  counter = 0
106
107
  collection.each do |resource|
107
- rslt = layout(resource.model).edit(resource.record_id, fm_params, :template=>self.class.fmresultset_template_path)
108
+ rslt = layout(resource.model).edit(resource.instance_variable_get(:@_record_id), fm_params)
108
109
  merge_fmp_response(resource, rslt[0])
109
110
  resource.persistence_state = DataMapper::Resource::PersistenceState::Clean.new resource
110
111
  counter +=1
@@ -129,7 +130,7 @@ module DataMapper
129
130
  def delete(collection)
130
131
  counter = 0
131
132
  collection.each do |resource|
132
- rslt = layout(resource.model).delete(resource.record_id, :template=>self.class.fmresultset_template_path)
133
+ rslt = layout(resource.model).delete(resource.instance_variable_get(:@_record_id))
133
134
  counter +=1
134
135
  end
135
136
  counter
@@ -150,11 +151,19 @@ module DataMapper
150
151
  prepend, append = options[:prepend], options[:append]
151
152
  fm_attributes = {}
152
153
  #puts "PREPARE FMP ATTRIBUTES"
153
- #DmProduct.last_query = attributes
154
- #y attributes.operands
154
+ #puts attributes.to_yaml
155
155
 
156
156
 
157
+ # Handles attributes that are relationships.
157
158
  attributes.dup.each do |key, val|
159
+ # If an attribute is a relationship,
160
+ # convert it to a key=>val that can be passed to Rfm.
161
+ # Note that DM will eager-load related records of a collection all at once,
162
+ # instead of one-at-a-time as you loop thru parent records.
163
+ # In this case, the val of a relationship attribute will be a collection of parent records,
164
+ # instead of just one parent record.
165
+ # Example: "product_type.each {|type| puts type.products.size}"
166
+ # Note that this variation will create n+1 queries "product_type.each {|type| puts type.products.count}"
158
167
  if key.class.name[/Relationship/]
159
168
  parent_keys = key.parent_key.to_a #.collect(){|p| p.name}
160
169
  child_keys = key.child_key.to_a #.collect(){|p| p.name}
@@ -162,18 +171,13 @@ module DataMapper
162
171
  #puts "RELATIONSHIP CHILD #{key.child_model_name} #{child_keys.inspect}"
163
172
  #puts "RELATIONSHIP CRITERIA #{val.inspect}"
164
173
  child_keys.each_with_index do |k, i|
165
- attributes[k] = val[parent_keys[i].name]
174
+ # The value dup is necessary, else the original data of parent resource will be modified.
175
+ attributes[k] = Array(val).collect{|v| v[parent_keys[i].name].tap{|x| x.dup rescue x} }
166
176
  attributes.delete key
167
177
  end
168
178
  end
169
179
  end
170
180
 
171
- # TODO: Handle attributes that have relationship components (major PITA!)
172
- # q.conditions.operands.to_a[0].subject .parent_key.collect {|p| p.name}
173
- # q.conditions.operands.to_a[0].subject .child_key.collect {|p| p.name}
174
- # q.conditions.operands.to_a[0].loaded_value[child-key-name]
175
- # new_attributes[child-key-name] = parent-key-value
176
-
177
181
  #puts "ATTRIBUTES BEFORE attributes_as_fields"
178
182
  #y attributes
179
183
  attributes_as_fields(attributes).each do |key, val|
@@ -222,7 +226,7 @@ module DataMapper
222
226
  # Class methods extended onto model subclass.
223
227
  module ModelMethods
224
228
  def layout
225
- @layout ||= Rfm.layout(storage_name, repository.adapter.options.symbolize_keys)
229
+ @layout ||= Rfm.layout(storage_name, repository.adapter.options.merge(FMRESULTSET_TEMPLATE).symbolize_keys)
226
230
  end
227
231
 
228
232
  # Not how to do this. Doesn't work anywhere I've tried it:
@@ -49,14 +49,9 @@ module DataMapper
49
49
  end
50
50
 
51
51
  module Model
52
+ # For testing adapter methods.
52
53
  attr_accessor :last_query
53
- alias_method :finalize_orig, :finalize
54
- def finalize(*args)
55
- property :record_id, Integer, :lazy=>false
56
- property :mod_id, Integer, :lazy=>false
57
- finalize_orig
58
- end
59
- end
54
+ end # Model
60
55
 
61
56
  class Query
62
57
  # Convert dm query conditions to fmp query params (hash)
@@ -141,4 +136,51 @@ module DataMapper
141
136
  end
142
137
 
143
138
  end # Query
144
- end # DataMapper
139
+ end # DataMapper
140
+
141
+ module Rfm
142
+ class Resultset
143
+
144
+ # Does custom processing during each record-to-resource translation done in DataMapper::Model#load
145
+ # Doing this here means we don't have to mess with DataMapper::Model#load.
146
+ def map
147
+ super do |record|
148
+ resource = yield(record)
149
+ #puts "DM INPUT RECORD: #{record.class} #{record.instance_variable_get(:@record_id)}"
150
+
151
+ if resource.kind_of?(DataMapper::Resource)
152
+ #puts "MODEL#LOAD custom processing RECORD #{record.class} RESOURCE #{resource.class}"
153
+ #puts record.inspect
154
+ # For Testing:
155
+ #resource.instance_variable_set(:@record, record)
156
+ # WBR - Loads portal data into DM model attached to this resource.
157
+ portals = record.instance_variable_get(:@portals)
158
+ #puts "MODEL#LOAD record #{record.class} portals #{portals.keys rescue 'no portals'}"
159
+ #if record.respond_to?(:portals) && record.portals.kind_of?(Hash) && record.portals.any?
160
+ model = resource.class
161
+ if portals.kind_of?(Hash) && portals.any?
162
+ begin
163
+ #puts record.portals.to_yaml
164
+ portal_keys = portals.keys
165
+ #puts "PORTALS: #{portal_keys}"
166
+ portal_keys.each do |portal_key|
167
+ #relat = model.relationships.to_a.find{|r| storage_name = r.child_model.storage_names[:default]; portal_key.to_s == storage_name }
168
+ relat = model.relationships.to_a.find{|r| storage_name = r.child_model.storage_name; portal_key.to_s == storage_name }
169
+ if relat
170
+ #puts "BUILDING RELATIONSHIP FROM PORTAL: #{relat.name} #{relat.child_model.name}"
171
+ resource.instance_variable_set(relat.instance_variable_name, relat.child_model.load(record.instance_variable_get(:@portals)[portal_key], relat.child_model.query) )
172
+ end
173
+ end
174
+ rescue
175
+ puts "ERROR LOADING PORTALS #{$!}"
176
+ end
177
+ end
178
+ resource.instance_variable_set(:@_record_id, record.instance_variable_get(:@record_id))
179
+ resource.instance_variable_set(:@_mod_id, record.instance_variable_get(:@mod_id))
180
+ end
181
+
182
+ resource
183
+ end
184
+ end
185
+ end
186
+ end
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # YAML structure defining a SAX parsing scheme for fmresultset xml.
3
3
  # The initial object of this parse should be a new instance of Rfm::Resultset.
4
+ # This template is intended for use with dm-filemaker-adapter, and not as
5
+ # a general Rfm::Resultset template.
4
6
  ---
5
7
  attach_elements: _meta
6
8
  attach_attributes: _meta
@@ -29,10 +31,6 @@ elements:
29
31
  - name: metadata
30
32
  attach: none
31
33
  - name: field_definition
32
- # These two steps can be used to create the attachment to resultset-meta automatically,
33
- # but the field-mapping translation won't happen.
34
- # attach: [_meta, 'Rfm::Metadata::Field', allocate]
35
- # as_name: field_meta
36
34
  attach: [cursor, 'Rfm::Metadata::Field', ':allocate']
37
35
  delimiter: name
38
36
  attach_attributes: private
@@ -56,28 +54,27 @@ elements:
56
54
  - name: fetch_size
57
55
  accessor: none
58
56
  - name: record
59
- #attach: [cursor, object, handle_new_record, _attributes]
60
- #attach_attributes: none
61
- attach: [array, 'Rfm::Record', new, object]
62
- attach_attributes: hash
63
- before_close: '@loaded=true'
57
+ #attach: [array, 'Rfm::Record', new, object]
58
+ attach: [array, 'Hash', new]
59
+ attach_attributes: private
60
+ compact: true
61
+ #before_close: '@loaded=true'
64
62
  elements:
65
63
  - name: field
66
64
  attach: [cursor, 'Rfm::Metadata::Datum', ':allocate']
67
65
  compact: false
68
66
  before_close: [object, field_element_close_callback, self]
69
67
  - name: relatedset
70
- attach: [private, Array, ':allocate']
68
+ attach: [private, 'Rfm::Resultset', ':allocate']
71
69
  as_name: portals
72
70
  attach_attributes: private
73
71
  create_accessors: all
74
72
  delimiter: table
75
73
  elements:
76
74
  - name: record
77
- #class: Rfm::Record
78
- attach: [default, 'Rfm::Record', ':allocate']
79
- attach_attributes: hash
80
- before_close: '@loaded=true'
75
+ attach: [default, 'Hash', ':allocate']
76
+ attach_attributes: private
77
+ #before_close: '@loaded=true'
81
78
  elements:
82
79
  - name: field
83
80
  compact: true
@@ -1,5 +1,5 @@
1
1
  module DataMapper
2
2
  module FilemakerAdapter
3
- VERSION = "0.0.3"
3
+ VERSION = "0.0.4"
4
4
  end
5
5
  end