dm-filemaker-adapter 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/README.md +46 -13
- data/lib/dm-filemaker-adapter.rb +1 -3
- data/lib/dm-filemaker-adapter/adapter.rb +64 -167
- data/lib/dm-filemaker-adapter/core_patches.rb +144 -0
- data/lib/dm-filemaker-adapter/version.rb +1 -1
- data/spec/dm-filemaker-adapter/adapter_spec.rb +170 -9
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NzA1ZDIzZGI0NWJkMDFmNjIyM2Q2NGMwMTQxZWM3MGMwYWU2ZDM0Mg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
M2M4OWE4Nzc1NmMyYTViNmI1NzAwZTFmNDlkNDUyZGY3MDAxMGJkOQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
Y2RmYTdjNjg2YzgwNTFkYzc1ODUyMTAyOThkMzc1ZDIwYTk0ZDA1ZTI1ZDhi
|
10
|
+
ZDRhODFhMjNlNzJhOGNjOTM0YmNkNmQyZjczYzdhMWU0NmVlNjcyM2Y4OTgz
|
11
|
+
NjQ1MDI1M2U1NDZkNzY5NmMxYzkzNDJjYTk4NTg4MGQ2YTFmYmQ=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YWI2ZjRhM2Q5MmYzOTdhOWM3NmNjNjM3ZDJlNzU1OGY4ZDZhYjQ2ZGMyNWM1
|
14
|
+
NjBlZGJkOWJmNDA1N2NlYTI0OGQ0M2FmODgzZGNjNGI4NTQ4MzVhN2IxYmQw
|
15
|
+
OTRmNjhkNzMyMmVmODFjZTI2NTFmODI1OGFiOTgyNmIwMDVlMjU=
|
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, 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.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -20,6 +20,8 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
+
# ruby
|
24
|
+
|
23
25
|
DB_CONFIG = {
|
24
26
|
adapter: 'filemaker',
|
25
27
|
host: 'my.server.com',
|
@@ -32,25 +34,56 @@ Or install it yourself as:
|
|
32
34
|
|
33
35
|
class User
|
34
36
|
include DataMapper::Resource
|
35
|
-
storage_names[:default] = 'user_xml' # This is
|
37
|
+
storage_names[:default] = 'user_xml' # This is the name of a filemaker layout representing the table you're modeling.
|
36
38
|
|
37
39
|
# Property & field names in this list must be lowercase, regardless of what they are in Filemaker.
|
38
40
|
|
39
|
-
property :
|
40
|
-
property :
|
41
|
-
|
42
|
-
property :
|
41
|
+
property :id, Serial
|
42
|
+
property :username, String, :length => 128, :unique => true, :required => true,
|
43
|
+
:default => lambda {|r,v| r.instance_variable_get :@email}
|
44
|
+
property :email, String, :length => 128, :unique => true, :required => true, :format=>:email_address
|
45
|
+
property :updated_at, DateTime, :field=>'modification_timestamp'
|
43
46
|
property :encrypted_password, BCryptPassword
|
44
47
|
end
|
45
48
|
|
46
49
|
DataMapper.finalize
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
|
52
|
+
|
53
|
+
# create records
|
54
|
+
User.create(:email => 'abc@company.com', :username => 'abc')
|
55
|
+
|
56
|
+
# get a specific user id
|
57
|
+
User.get '1035'
|
58
|
+
|
59
|
+
# first record that matches exactly 'name'
|
60
|
+
User.first :username => 'name'
|
61
|
+
|
62
|
+
# all records updated since 3 days ago
|
63
|
+
User.all :updated.gt => Time.now-3*24*60*60
|
64
|
+
|
65
|
+
# records 10 thru 20, ordered by :id (the range is resolved by filemaker, before records are returned!)
|
66
|
+
User.all(:order => :id)[10..20]
|
67
|
+
|
68
|
+
# use the union operator to create 2 find requests in a filemaker 'OR' operation
|
69
|
+
User.all(:email => 'abc@company.com', :activated_at.gt => '1/1/1980') | \
|
70
|
+
User.all(:username => 'abc', :activated_at.gt => '1/1/1980')
|
71
|
+
|
72
|
+
# 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
|
+
]
|
77
|
+
|
78
|
+
# use the intersection operator to combine multiple search criteria in a filemaker 'AND' operation
|
79
|
+
User.all(:email => 'abc@company.com', :activated_at.gt => '1/1/2015') & \
|
80
|
+
User.all(:email => 'abc@company.com', :activated_at.lt => '5/1/2015')
|
81
|
+
|
82
|
+
# you can also write this as
|
83
|
+
User.all(:email => 'abc@company.com', :activated_at.gt => '1/1/2015', :activated_at.lt => '5/1/2015')
|
55
84
|
|
85
|
+
# 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')
|
87
|
+
|
88
|
+
|
56
89
|
|
data/lib/dm-filemaker-adapter.rb
CHANGED
@@ -1,35 +1,6 @@
|
|
1
1
|
# Property & field names in dm-filemaker-adapter models must be declared lowercase, regardless of what they are in FMP.
|
2
|
-
|
2
|
+
require 'dm-filemaker-adapter/core_patches'
|
3
3
|
module DataMapper
|
4
|
-
[Resource, Model, Adapters]
|
5
|
-
|
6
|
-
# All this to tack on class and instance methods to the model/resource.
|
7
|
-
module Resource
|
8
|
-
class << self
|
9
|
-
alias_method :included_orig, :included
|
10
|
-
def included(klass)
|
11
|
-
included_orig(klass)
|
12
|
-
if klass.repository.adapter.to_s[/filemaker/i]
|
13
|
-
klass.instance_eval do
|
14
|
-
extend repository.adapter.class::ModelMethods
|
15
|
-
include repository.adapter.class::ResourceMethods
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
module Model
|
23
|
-
#attr_accessor :last_query
|
24
|
-
alias_method :finalize_orig, :finalize
|
25
|
-
def finalize(*args)
|
26
|
-
property :record_id, Integer, :lazy=>false
|
27
|
-
property :mod_id, Integer, :lazy=>false
|
28
|
-
finalize_orig
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
|
33
4
|
|
34
5
|
module Adapters
|
35
6
|
|
@@ -37,24 +8,6 @@ module DataMapper
|
|
37
8
|
@fmresultset_template_path = File.expand_path('../dm-fmresultset.yml', __FILE__).to_s
|
38
9
|
class << self; attr_accessor :fmresultset_template_path; end
|
39
10
|
VERSION = DataMapper::FilemakerAdapter::VERSION
|
40
|
-
|
41
|
-
|
42
|
-
### UTILITY METHODS ###
|
43
|
-
|
44
|
-
# Class methods extended onto model.
|
45
|
-
module ModelMethods
|
46
|
-
def layout
|
47
|
-
Rfm.layout(storage_name, repository.adapter.options.symbolize_keys)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Instance methods included in model.
|
52
|
-
module ResourceMethods
|
53
|
-
def layout
|
54
|
-
model.layout
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
11
|
|
59
12
|
|
60
13
|
### ADAPTER CORE METHODS ###
|
@@ -74,10 +27,10 @@ module DataMapper
|
|
74
27
|
#
|
75
28
|
# @api semipublic
|
76
29
|
def create(resources)
|
77
|
-
|
30
|
+
resources[0].model.last_query = resources
|
78
31
|
counter = 0
|
79
32
|
resources.each do |resource|
|
80
|
-
fm_params =
|
33
|
+
fm_params = prepare_fmp_attributes(resource.dirty_attributes)
|
81
34
|
rslt = layout(resource.model).create(fm_params, :template=>self.class.fmresultset_template_path)
|
82
35
|
merge_fmp_response(resource, rslt[0])
|
83
36
|
counter +=1
|
@@ -104,12 +57,14 @@ module DataMapper
|
|
104
57
|
# end
|
105
58
|
#
|
106
59
|
def read(query)
|
107
|
-
|
60
|
+
query.model.last_query = query
|
108
61
|
#y query
|
109
62
|
_layout = layout(query.model)
|
110
|
-
opts = fmp_options
|
63
|
+
opts = query.fmp_options
|
64
|
+
#puts "FMP OPTIONS #{opts.inspect}"
|
111
65
|
opts[:template] = self.class.fmresultset_template_path
|
112
|
-
prms =
|
66
|
+
prms = query.to_fmp_query
|
67
|
+
#puts "ADAPTER#read fmp_query built: #{prms.inspect}"
|
113
68
|
rslt = prms.empty? ? _layout.all(opts) : _layout.find(prms, opts)
|
114
69
|
rslt.dup.each_with_index(){|r, i| rslt[i] = r.to_h}
|
115
70
|
rslt
|
@@ -118,12 +73,12 @@ module DataMapper
|
|
118
73
|
# Takes a query and returns number of matched records.
|
119
74
|
# An empty query will return the total record count
|
120
75
|
def aggregate(query)
|
121
|
-
|
76
|
+
query.model.last_query = query
|
122
77
|
#y query
|
123
78
|
_layout = layout(query.model)
|
124
|
-
opts = fmp_options
|
79
|
+
opts = query.fmp_options
|
125
80
|
opts[:template] = self.class.fmresultset_template_path
|
126
|
-
prms = fmp_query(query.conditions)
|
81
|
+
prms = fmp_query(query.conditions)
|
127
82
|
#[prms.empty? ? _layout.all(:max_records=>0).foundset_count : _layout.count(prms)]
|
128
83
|
[prms.empty? ? _layout.view.total_count : _layout.count(prms)]
|
129
84
|
end
|
@@ -145,8 +100,8 @@ module DataMapper
|
|
145
100
|
#
|
146
101
|
# @api semipublic
|
147
102
|
def update(attributes, collection)
|
148
|
-
|
149
|
-
fm_params =
|
103
|
+
collection[0].model.last_query = [attributes, collection]
|
104
|
+
fm_params = prepare_fmp_attributes(attributes)
|
150
105
|
counter = 0
|
151
106
|
collection.each do |resource|
|
152
107
|
rslt = layout(resource.model).edit(resource.record_id, fm_params, :template=>self.class.fmresultset_template_path)
|
@@ -182,89 +137,41 @@ module DataMapper
|
|
182
137
|
|
183
138
|
|
184
139
|
|
185
|
-
### ADAPTER HELPER METHODS
|
140
|
+
### ADAPTER HELPER METHODS & UTILITIES ###
|
186
141
|
|
187
142
|
# Create fmp layout object from model object.
|
188
143
|
def layout(model)
|
189
144
|
#Rfm.layout(model.storage_name, options.symbolize_keys) #query.repository.adapter.options.symbolize_keys)
|
190
145
|
model.layout
|
191
146
|
end
|
192
|
-
|
193
|
-
# Convert dm query object to fmp query params (hash)
|
194
|
-
def fmp_query(input)
|
195
|
-
#puts "CONDITIONS input #{input.class.name} (#{input})"
|
196
|
-
if input.class.name[/OrOperation/]
|
197
|
-
input.operands.collect {|o| fmp_query o}
|
198
|
-
elsif input.class.name[/AndOperation/]
|
199
|
-
h = Hash.new
|
200
|
-
input.operands.each do |k,v|
|
201
|
-
r = fmp_query(k)
|
202
|
-
#puts "CONDITIONS operand #{r}"
|
203
|
-
if r.is_a?(Hash)
|
204
|
-
h.merge!(r)
|
205
|
-
else
|
206
|
-
h=r
|
207
|
-
break
|
208
|
-
end
|
209
|
-
end
|
210
|
-
h
|
211
|
-
elsif input.class.name[/NullOperation/] || input.nil?
|
212
|
-
{}
|
213
|
-
else
|
214
|
-
#puts "FMP_QUERY OPERATION #{input.class}"
|
215
|
-
val = input.loaded_value
|
216
147
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
attributes.to_h.each do |k,v|
|
245
|
-
fm_params[k.field] = v.respond_to?(:_to_fm) ? v._to_fm : v
|
246
|
-
end
|
247
|
-
# fm_params = Hash.new
|
248
|
-
# resource.dirty_attributes.each do |a,v|
|
249
|
-
# fm_params[a.field] = v.respond_to?(:_to_fm) ? v._to_fm : v
|
250
|
-
# end
|
251
|
-
fm_params
|
252
|
-
end
|
253
|
-
|
254
|
-
# Get fmp options hash from query
|
255
|
-
def fmp_options(query)
|
256
|
-
fm_options = {}
|
257
|
-
fm_options[:skip_records] = query.offset if query.offset
|
258
|
-
fm_options[:max_records] = query.limit if query.limit
|
259
|
-
if query.order
|
260
|
-
fm_options[:sort_field] = query.order.collect do |ord|
|
261
|
-
ord.target.field
|
262
|
-
end
|
263
|
-
fm_options[:sort_order] = query.order.collect do |ord|
|
264
|
-
ord.operator.to_s + 'end'
|
265
|
-
end
|
266
|
-
end
|
267
|
-
fm_options
|
148
|
+
def prepare_fmp_attributes(attributes, *args)
|
149
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
150
|
+
prepend, append = options[:prepend], options[:append]
|
151
|
+
fm_attributes = {}
|
152
|
+
#puts "PREPARE FMP ATTRIBUTES"
|
153
|
+
#y attributes
|
154
|
+
attributes_as_fields(attributes).each do |key, val|
|
155
|
+
#puts "EACH ATTRIBUTE class #{val.class}"
|
156
|
+
#puts "EACH ATTRIBUTE value #{val}"
|
157
|
+
new_val = val && [val.is_a?(Fixnum) ? val : val.dup].flatten.inject([]) do |r, v|
|
158
|
+
#puts "INJECTING v"
|
159
|
+
#puts v
|
160
|
+
new_v = v.respond_to?(:_to_fm) ? v._to_fm : v
|
161
|
+
#puts "CONVERTING VAL #{new_val} TO STRING"
|
162
|
+
new_v = new_v.to_s
|
163
|
+
#puts "PREPENDING #{new_v} with '#{prepend}'"
|
164
|
+
new_v.prepend prepend if prepend rescue nil
|
165
|
+
new_v.append append if append rescue nil
|
166
|
+
r << new_v
|
167
|
+
end
|
168
|
+
#puts "NEW_VAL"
|
169
|
+
#puts new_val
|
170
|
+
fm_attributes[key] = (new_val && new_val.size < 2) ? new_val[0] : new_val
|
171
|
+
end
|
172
|
+
#puts "FM_ATTRIBUTES"
|
173
|
+
#puts fm_attributes
|
174
|
+
fm_attributes
|
268
175
|
end
|
269
176
|
|
270
177
|
def merge_fmp_response(resource, record)
|
@@ -281,40 +188,30 @@ module DataMapper
|
|
281
188
|
# field
|
282
189
|
# end
|
283
190
|
|
284
|
-
|
285
|
-
|
191
|
+
protected :merge_fmp_response #,:fmp_options, :prepare_fmp_attributes, :fmp_operator,:fmp_query
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
### LOADED WHEN RESOURCES ARE INCLUDED ###
|
196
|
+
|
197
|
+
# Class methods extended onto model subclass.
|
198
|
+
module ModelMethods
|
199
|
+
def layout
|
200
|
+
@layout ||= Rfm.layout(storage_name, repository.adapter.options.symbolize_keys)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Not how to do this. Doesn't work anywhere I've tried it:
|
204
|
+
#extend Forwardable
|
205
|
+
#def_delegators :layout, *layout.class.instance_methods.select {|m| m.to_s[/^[a-z]/]}
|
206
|
+
end
|
207
|
+
|
208
|
+
# Instance methods included in model.
|
209
|
+
module ResourceMethods
|
210
|
+
def layout
|
211
|
+
model.layout
|
212
|
+
end
|
213
|
+
end
|
286
214
|
|
287
215
|
end # FilemakerAdapter
|
288
216
|
end # Adapters
|
289
217
|
end # DataMapper
|
290
|
-
|
291
|
-
class Time
|
292
|
-
def _to_fm
|
293
|
-
d = strftime('%m/%d/%Y') unless Date.today == Date.parse(self.to_s)
|
294
|
-
t = strftime('%T')
|
295
|
-
d ? "#{d} #{t}" : t
|
296
|
-
end
|
297
|
-
end # Time
|
298
|
-
|
299
|
-
class DateTime
|
300
|
-
def _to_fm
|
301
|
-
d = strftime('%m/%d/%Y')
|
302
|
-
t =strftime('%T')
|
303
|
-
"#{d} #{t}"
|
304
|
-
end
|
305
|
-
end # Time
|
306
|
-
|
307
|
-
class Timestamp
|
308
|
-
def _to_fm
|
309
|
-
d = strftime('%m/%d/%Y')
|
310
|
-
t =strftime('%T')
|
311
|
-
"#{d} #{t}"
|
312
|
-
end
|
313
|
-
end # Time
|
314
|
-
|
315
|
-
class Date
|
316
|
-
def _to_fm
|
317
|
-
strftime('%m/%d/%Y')
|
318
|
-
end
|
319
|
-
end # Time
|
320
|
-
|
@@ -0,0 +1,144 @@
|
|
1
|
+
class Time
|
2
|
+
def _to_fm
|
3
|
+
d = strftime('%m/%d/%Y') #unless Date.today == Date.parse(self.to_s)
|
4
|
+
t = strftime('%T')
|
5
|
+
d ? "#{d} #{t}" : t
|
6
|
+
end
|
7
|
+
end # Time
|
8
|
+
|
9
|
+
class DateTime
|
10
|
+
def _to_fm
|
11
|
+
d = strftime('%m/%d/%Y')
|
12
|
+
t =strftime('%T')
|
13
|
+
"#{d} #{t}"
|
14
|
+
end
|
15
|
+
end # Time
|
16
|
+
|
17
|
+
class Timestamp
|
18
|
+
def _to_fm
|
19
|
+
d = strftime('%m/%d/%Y')
|
20
|
+
t =strftime('%T')
|
21
|
+
"#{d} #{t}"
|
22
|
+
end
|
23
|
+
end # Time
|
24
|
+
|
25
|
+
class Date
|
26
|
+
def _to_fm
|
27
|
+
strftime('%m/%d/%Y')
|
28
|
+
end
|
29
|
+
end # Time
|
30
|
+
|
31
|
+
|
32
|
+
module DataMapper
|
33
|
+
[Adapters, Model, Query, Resource]
|
34
|
+
|
35
|
+
# All this to tack on class and instance methods to the model/resource.
|
36
|
+
module Resource
|
37
|
+
class << self
|
38
|
+
alias_method :included_orig, :included
|
39
|
+
def included(klass)
|
40
|
+
included_orig(klass)
|
41
|
+
if klass.repository.adapter.to_s[/filemaker/i]
|
42
|
+
klass.instance_eval do
|
43
|
+
extend repository.adapter.class::ModelMethods
|
44
|
+
include repository.adapter.class::ResourceMethods
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Model
|
52
|
+
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
|
60
|
+
|
61
|
+
class Query
|
62
|
+
# Convert dm query conditions to fmp query params (hash)
|
63
|
+
def to_fmp_query(input=self.conditions)
|
64
|
+
#puts "FMP_QUERY input #{input.class.name}"
|
65
|
+
rslt = if input.class.name[/OrOperation/]
|
66
|
+
#puts "FMP_QUERY OrOperation #{input.class}"
|
67
|
+
input.operands.collect do |o|
|
68
|
+
r = to_fmp_query o
|
69
|
+
#puts "FMP_QUERY or-operation operand #{r}"
|
70
|
+
r
|
71
|
+
end
|
72
|
+
elsif input.class.name[/AndOperation/]
|
73
|
+
#puts "FMP_QUERY AndOperation input class #{input.class}"
|
74
|
+
#puts "FMP_QUERY AndOperation input value #{input.inspect}"
|
75
|
+
out = {}
|
76
|
+
input.operands.each do |k,v|
|
77
|
+
#puts "FMP_QUERY and-operation pre-process operand key:val #{k}:#{v}"
|
78
|
+
r = to_fmp_query(k).to_hash
|
79
|
+
#puts "FMP_QUERY and-operation post-process operand #{r}"
|
80
|
+
if r.is_a?(Hash)
|
81
|
+
#puts "FMP_QUERY and-operation operand is a hash"
|
82
|
+
# Filemaker can't have the same field twice in a single find request,
|
83
|
+
# but we can mash the two conditions together in a way that FMP can use.
|
84
|
+
out.merge!(r){|k, oldv, newv| "#{oldv} #{newv}"}
|
85
|
+
else
|
86
|
+
#puts "FMP_QUERY and-operation operand is NOT a hash"
|
87
|
+
out = r
|
88
|
+
break
|
89
|
+
end
|
90
|
+
end
|
91
|
+
out
|
92
|
+
elsif input.class.name[/NullOperation/] || input.nil?
|
93
|
+
#puts "FMP_QUERY NullOperation #{input.class}"
|
94
|
+
{}
|
95
|
+
else
|
96
|
+
#puts "FMP_QUERY else input class #{input.class}"
|
97
|
+
#puts "FMP_QUERY else input value #{input.inspect}"
|
98
|
+
#puts "FMP_QUERY else-options #{self.options.inspect}"
|
99
|
+
#prepare_fmp_attributes({input.subject=>input.value}, :prepend=>fmp_operator(input.class.name))
|
100
|
+
value = (
|
101
|
+
self.options[input.keys[0]] ||
|
102
|
+
self.options[input.subject.name] ||
|
103
|
+
self.options.find{|o,v| o.respond_to?(:target) && o.target.to_s == input.subject.name.to_s}[1] ||
|
104
|
+
input.value
|
105
|
+
) rescue input.value #(puts "ERROR #{$!}"; input.value)
|
106
|
+
#puts "FMP_QUERY else-value #{value}"
|
107
|
+
repository.adapter.prepare_fmp_attributes({input.subject=>value}, :prepend=>fmp_operator(input.class.name))
|
108
|
+
end
|
109
|
+
#puts "FMP_QUERY output #{rslt.inspect}"
|
110
|
+
rslt
|
111
|
+
end # to_fmp_query
|
112
|
+
|
113
|
+
# Convert operation class to operator string
|
114
|
+
def fmp_operator(operation)
|
115
|
+
case
|
116
|
+
when operation[/GreaterThanOrEqualTo/]; '>='
|
117
|
+
when operation[/LessThanOrEqualTo/]; '<='
|
118
|
+
when operation[/GreaterThan/]; '>'
|
119
|
+
when operation[/LessThan/]; '<'
|
120
|
+
when operation[/EqualTo/]; '=='
|
121
|
+
when operation[/Like/];
|
122
|
+
when operation[/Null/];
|
123
|
+
else nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Get fmp options hash from query
|
128
|
+
def fmp_options(query=self)
|
129
|
+
fm_options = {}
|
130
|
+
fm_options[:skip_records] = query.offset if query.offset
|
131
|
+
fm_options[:max_records] = query.limit if query.limit
|
132
|
+
if query.order
|
133
|
+
fm_options[:sort_field] = query.order.collect do |ord|
|
134
|
+
ord.target.field
|
135
|
+
end
|
136
|
+
fm_options[:sort_order] = query.order.collect do |ord|
|
137
|
+
ord.operator.to_s + 'end'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
fm_options
|
141
|
+
end
|
142
|
+
|
143
|
+
end # Query
|
144
|
+
end # DataMapper
|
@@ -1,11 +1,172 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
# Rspec Issues (maybe not bugs, but certainly not expected behavior!).
|
4
|
+
#
|
5
|
+
# expect/allow_any_instance_of does not work with block form of arguments in '...to receive(:message) do |args|
|
6
|
+
# The args will always just be the instance object.
|
7
|
+
#
|
8
|
+
# and_call_original breaks block form of expect.
|
9
|
+
#
|
10
|
+
|
11
|
+
# Shared Example examples:
|
12
|
+
# RSpec.shared_examples "#to_fmp_query" do
|
13
|
+
# it 'Receives dm query conditions and returns fmp query' do
|
14
|
+
# allow_any_instance_of(Rfm::Layout).to receive(:find).and_return(Rfm::Resultset.allocate)
|
15
|
+
# expect_any_instance_of(DataMapper::Query).to receive(:to_fmp_query).at_least(:once).and_call_original
|
16
|
+
# query.inspect
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# context "with simple .all query" do
|
21
|
+
# include_examples "#to_fmp_query" do
|
22
|
+
# let(:query){User.all(:id=>1)}
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
|
26
|
+
describe DataMapper do
|
27
|
+
before :each do
|
28
|
+
DataMapper.setup(:default, 'filemaker://user:pass@hostname.com/DatabaseName')
|
29
|
+
class ::User
|
30
|
+
include DataMapper::Resource
|
31
|
+
property :id, Serial
|
32
|
+
property :email, String
|
33
|
+
property :username, String
|
34
|
+
property :activated_at, DateTime
|
35
|
+
|
36
|
+
has n, :orders
|
37
|
+
end
|
38
|
+
class ::Order
|
39
|
+
include DataMapper::Resource
|
40
|
+
property :id, Serial
|
41
|
+
property :total, Decimal
|
42
|
+
|
43
|
+
belongs_to :user
|
44
|
+
end
|
45
|
+
DataMapper.finalize
|
46
|
+
end
|
47
|
+
|
48
|
+
describe DataMapper::Adapters::FilemakerAdapter do
|
49
|
+
|
50
|
+
it 'Has a version number' do
|
51
|
+
expect(DataMapper::Adapters::FilemakerAdapter::VERSION).not_to be nil
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'Does something useful' do
|
55
|
+
expect(true).to eq(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'Supports model classes' do
|
59
|
+
expect(User.ancestors.include?(DataMapper::Adapters::FilemakerAdapter::ResourceMethods)).to eq(true)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'Acts as dm database adapter for model repository' do
|
63
|
+
expect(User.repository.adapter.class).to eq(DataMapper::Adapters::FilemakerAdapter)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
# THESE ARE ALL WRONG. They should process and return real data, instead of returning canned 'safe' responses with rspec mocking.
|
69
|
+
# Use .and_call_original to allow expected methods to run their original purpose.
|
70
|
+
# See https://github.com/rspec/rspec-mocks.
|
71
|
+
describe '#read' do
|
72
|
+
it 'Receives dm query object with conditions' do
|
73
|
+
#expect_any_instance_of(DataMapper::Adapters::FilemakerAdapter).to receive(:read).and_return(Rfm::Resultset.allocate)
|
74
|
+
expect(User.repository.adapter).to receive(:read) do |query|
|
75
|
+
expect(query.class).to eq(DataMapper::Query)
|
76
|
+
expect(query.conditions.first.subject.field).to eq('id')
|
77
|
+
expect(query.conditions.first.value).to eq(1)
|
78
|
+
end.and_return(Rfm::Resultset.allocate)
|
79
|
+
User.all(:id=>1).inspect
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end # datamapper-adapters-filemaker
|
84
|
+
|
85
|
+
describe DataMapper::Query do
|
86
|
+
|
87
|
+
describe '#to_fmp_query' do
|
88
|
+
# I'm passing in the example here, so we can use the metadata to construct
|
89
|
+
# streamlined tests for a variety of query inputs to the to_fmp_query method.
|
90
|
+
before(:each) do |example|
|
91
|
+
|
92
|
+
# Not really needed yet, but helpful if calls go into Rfm.
|
93
|
+
allow_any_instance_of(Rfm::Layout).to receive(:find).and_return(Rfm::Resultset.allocate)
|
94
|
+
#expect_any_instance_of(DataMapper::Query).to receive(:to_fmp_query).at_least(:once).and_call_original
|
95
|
+
|
96
|
+
# Capture the query object passed to the adapter#read method
|
97
|
+
expect(DataMapper.repository.adapter).to receive(:read) do |query|
|
98
|
+
#puts "QUERY #{query}"
|
99
|
+
#puts "Self within before/expect block #{self}"
|
100
|
+
@query = query
|
101
|
+
#puts @query.conditions.to_yaml
|
102
|
+
# TODO: See rspec mocks docs for how to pass control to the original method and get a result back right here.
|
103
|
+
# hint - it does something like this: @original_method = adapter.method(:to_fmp_query); @original_method.call(query)
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Possible helpful meta info for each example.
|
108
|
+
#puts "EXAMPLE INSTANCE VARS #{example.instance_variables.inspect}"
|
109
|
+
#
|
110
|
+
# Uses the desription of the example as the user query code,
|
111
|
+
# and uses the block of the example as the expected to_fmp_query result.
|
112
|
+
# Also replaces the description with more informative info, including the expected result.
|
113
|
+
#
|
114
|
+
@name = example.description.dup
|
115
|
+
@block = example.instance_variable_get(:@example_block)
|
116
|
+
@expected_result = @block.call
|
117
|
+
example.description.replace "#{@name} should return #{@expected_result}"
|
118
|
+
# Runs the query to get the query object.
|
119
|
+
eval(@name.to_s).inspect
|
120
|
+
@fmp_query = @query.to_fmp_query
|
121
|
+
# All of the above, so we can do this.
|
122
|
+
expect(@fmp_query).to eq(@expected_result)
|
123
|
+
end
|
124
|
+
|
125
|
+
context "Simple .all" do
|
126
|
+
it('User.all'){ {} }
|
127
|
+
end
|
128
|
+
|
129
|
+
context "Simple .first plus less-than comparison on time" do
|
130
|
+
it('User.first(:id=>1)') { {'id' => '==1'} }
|
131
|
+
it('User.first(:activated_at.lt=>"1/1/2015 00:00:00")') { {"activated_at"=>"<01/01/2015 00:00:00"} }
|
132
|
+
end
|
133
|
+
|
134
|
+
context "Compound OR with simple comparison on integer" do
|
135
|
+
it('(User.all(:id=>1) | User.all(:id=>2))') { [{"id"=>"==1"}, {"id"=>"==2"}] }
|
136
|
+
end
|
137
|
+
|
138
|
+
context "Simple .all containing duplicate keys with differing operators" do
|
139
|
+
it('User.all(:id.gt=>1, :id.lt=>5)') { {"id"=>">1 <5"} }
|
140
|
+
end
|
141
|
+
|
142
|
+
context "Compound AND operation" do
|
143
|
+
it('(User.all(:id=>1) & User.all(:email=>"some_email"))') { {"id"=>"==1", "email"=>"==some_email"} }
|
144
|
+
end
|
145
|
+
|
146
|
+
context "Simple implied .in operation" do
|
147
|
+
it('User.all(:id=>[1, 3, 5, 9])') { {"id"=>["1", "3", "5", "9"]} }
|
148
|
+
end
|
149
|
+
|
150
|
+
context "Compound OR with dup keys with differing operators and .gte comparison" do
|
151
|
+
it("(User.all(:username=>'uname', :activated_at.gt=>Time.parse('1970-01-01 00:00:00')) | User.all(:email.like=>'uname', :activated_at.gte=>Time.parse('1970-03-01 00:00:00')))"){
|
152
|
+
[{"username"=>"==uname", "activated_at"=>">01/01/1970 00:00:00"}, {"email"=>"uname", "activated_at"=>">=03/01/1970 00:00:00"}]
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
context "Compound AND with dup keys for a date range with .gte and .lte comparison" do
|
157
|
+
it('User.all(:activated_at.gte=>Time.parse("2015-01-01 00:00:00")) & User.all(:activated_at.lte=>Time.parse("2015-03-01 00:00:00"))') {
|
158
|
+
{"activated_at"=>">=01/01/2015 00:00:00 <=03/01/2015 00:00:00"}
|
159
|
+
}
|
160
|
+
end
|
161
|
+
# TODO: test field-name-translation
|
162
|
+
# TODO: test all other operator possibilities
|
163
|
+
|
164
|
+
# This tests nested associational query, which dm-filemaker-adapter cannot yet do.
|
165
|
+
#it('User.all(:email=>"something@dot.com", :orders=>{:total.gt=>10.0})') {{}}
|
166
|
+
|
167
|
+
|
168
|
+
end #to_fmp_query
|
169
|
+
|
170
|
+
end # datamapper-query
|
171
|
+
|
172
|
+
end # datamapper
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-filemaker-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- William Richardson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: data_mapper
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- dm-filemaker-adapter.gemspec
|
97
97
|
- lib/dm-filemaker-adapter.rb
|
98
98
|
- lib/dm-filemaker-adapter/adapter.rb
|
99
|
+
- lib/dm-filemaker-adapter/core_patches.rb
|
99
100
|
- lib/dm-filemaker-adapter/dm-fmresultset.yml
|
100
101
|
- lib/dm-filemaker-adapter/version.rb
|
101
102
|
- spec/dm-filemaker-adapter/adapter_spec.rb
|