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 +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
|