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 +8 -8
- data/Gemfile +2 -2
- data/README.md +31 -11
- data/Rakefile +1 -1
- data/lib/dm-filemaker-adapter/adapter.rb +30 -26
- data/lib/dm-filemaker-adapter/core_patches.rb +50 -8
- data/lib/dm-filemaker-adapter/dm-fmresultset.yml +11 -14
- data/lib/dm-filemaker-adapter/version.rb +1 -1
- data/spec/data/resultset_with_duplicate_portals.xml +1099 -0
- data/spec/data/resultset_with_portals.xml +866 -0
- data/spec/dm-filemaker-adapter/adapter_spec.rb +104 -31
- data/spec/spec_helper.rb +59 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NDE2YzRlM2E5YjU3NzFjYjAyZjM2NGVhMzE4YjExM2U4MjVmOGI2NA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ZTkzMmU3MmM2MmVlNmVkYmI2N2RiOTUwYzc5MDNlZWIyZTY5N2VlNg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZmMzZDIyNTgyZTZlYmQwZGU3M2U2YTg2MzcxMGFhMzRkODM0MzhiZDRhMjEz
|
10
|
+
NzZkYmExZjEzNTAwNmE2ZDk4Y2YwMzNjZWMyYTJhMzAzNjg0NTljOTM0YTYz
|
11
|
+
ZDgzMjczZWExYTk4NmFmNTlkYzZlMjZmMmE4MDU5NTMzZWNkYjk=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MTg2MDViODIyNTU4NzRiZGUxOTkzZWRlYjFkODcxYjhlMzVlYzZjMGU4NzJk
|
14
|
+
N2E4NWYxYWQ2NmRiMjQ4N2ZlMjlhNmNkOWM2ZTFiNDI5NjkyN2ViZWU5OTg5
|
15
|
+
Y2NmY2JiODY3NWM0ZmI4NDFkYjRlMWUxMTBjM2JjM2ExYTkzNzA=
|
data/Gemfile
CHANGED
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,
|
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
|
-
#
|
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=>'
|
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
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
106
|
+
User.find(:email => 'abc@company.com', :activated_at => '>1/1/2015 <5/1/2015')
|
87
107
|
|
88
108
|
|
89
109
|
|
data/Rakefile
CHANGED
@@ -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
|
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
|
-
#
|
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 "
|
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#
|
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
|
-
|
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.
|
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.
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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: [
|
60
|
-
|
61
|
-
|
62
|
-
|
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,
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|