dm-filemaker-adapter 0.0.3 → 0.0.4

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