dm-persevere-adapter 0.71.4 → 0.72.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +16 -0
- data/README.txt +1 -1
- data/Rakefile +17 -3
- data/VERSION +1 -1
- data/lib/persevere_adapter.rb +13 -778
- data/lib/persevere_adapter/adapter.rb +544 -0
- data/lib/persevere_adapter/aggregates.rb +75 -0
- data/lib/persevere_adapter/enhance.rb +49 -0
- data/lib/persevere_adapter/json_support.rb +6 -0
- data/lib/persevere_adapter/json_support/core.rb +17 -0
- data/lib/persevere_adapter/json_support/model.rb +22 -0
- data/lib/persevere_adapter/json_support/model/properties.rb +23 -0
- data/lib/persevere_adapter/json_support/property.rb +23 -0
- data/lib/persevere_adapter/json_support/resource.rb +37 -0
- data/lib/persevere_adapter/migrations.rb +104 -0
- data/lib/persevere_adapter/query.rb +208 -0
- data/lib/persevere_adapter/support/big_decimal.rb +10 -0
- data/lib/{persevere.rb → persevere_client.rb} +2 -2
- data/spec/persevere_adapter_spec.rb +508 -488
- data/spec/persevere_client_spec.rb +202 -0
- data/spec/spec_helper.rb +12 -3
- data/tasks/spec.rake +0 -1
- metadata +159 -24
- data/lib/dm/associations/many_to_many.rb +0 -278
- data/lib/dm/associations/relationship.rb +0 -57
- data/lib/dm/model.rb +0 -29
- data/lib/dm/property.rb +0 -25
- data/lib/dm/query.rb +0 -174
- data/lib/dm/resource.rb +0 -102
- data/spec/persevere_spec.rb +0 -201
data/.gitignore
CHANGED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source :rubyforge
|
2
|
+
|
3
|
+
gem "dm-core", "~> 1.0"
|
4
|
+
gem "dm-aggregates", "~> 1.0"
|
5
|
+
gem "dm-types", "~> 1.0"
|
6
|
+
gem "dm-migrations"
|
7
|
+
gem "dm-validations"
|
8
|
+
|
9
|
+
group :development do
|
10
|
+
gem "jeweler"
|
11
|
+
gem "rspec"
|
12
|
+
gem "yard"
|
13
|
+
gem "ruby-debug"
|
14
|
+
gem "addressable"
|
15
|
+
gem "rake", :require => nil
|
16
|
+
end
|
data/README.txt
CHANGED
@@ -41,7 +41,7 @@ end
|
|
41
41
|
To use with Rails, you can put this in your environment.rb:
|
42
42
|
config.gem "dm-core"
|
43
43
|
config.gem "data_objects"
|
44
|
-
config.gem "dm-persevere-adapter", :
|
44
|
+
config.gem "dm-persevere-adapter", :require => nil
|
45
45
|
|
46
46
|
With a database.yml:
|
47
47
|
|
data/Rakefile
CHANGED
@@ -2,6 +2,12 @@ require 'rubygems'
|
|
2
2
|
require 'rake'
|
3
3
|
require 'pathname'
|
4
4
|
|
5
|
+
begin
|
6
|
+
require 'bundler'
|
7
|
+
rescue LoadError
|
8
|
+
puts "Bundler is not intalled. Install with: gem install bundler"
|
9
|
+
end
|
10
|
+
|
5
11
|
begin
|
6
12
|
require 'jeweler'
|
7
13
|
Jeweler::Tasks.new do |gemspec|
|
@@ -10,10 +16,9 @@ begin
|
|
10
16
|
gemspec.description = %q{A DataMapper Adapter for persevere}
|
11
17
|
gemspec.email = ["irjudson [a] gmail [d] com"]
|
12
18
|
gemspec.homepage = %q{http://github.com/yogo/dm-persevere-adapter}
|
13
|
-
gemspec.authors = ["Ivan R. Judson", "The Yogo Data Management Development Team" ]
|
19
|
+
gemspec.authors = ["Ivan R. Judson", "Ryan Heimbuch", "The Yogo Data Management Development Team" ]
|
14
20
|
gemspec.rdoc_options = ["--main", "README.txt"]
|
15
|
-
gemspec.
|
16
|
-
gemspec.add_dependency(%q<extlib>)
|
21
|
+
gemspec.add_bundler_dependencies
|
17
22
|
end
|
18
23
|
|
19
24
|
Jeweler::GemcutterTasks.new
|
@@ -21,6 +26,15 @@ rescue LoadError
|
|
21
26
|
puts "Jeweler not available. Install it with: gem install jeweler"
|
22
27
|
end
|
23
28
|
|
29
|
+
begin
|
30
|
+
require 'yard'
|
31
|
+
YARD::Rake::YardocTask.new
|
32
|
+
rescue LoadError
|
33
|
+
task :yardoc do
|
34
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
24
38
|
FileList['tasks/**/*.rake'].each { |task| import task }
|
25
39
|
|
26
40
|
ROOT = Pathname(__FILE__).dirname.expand_path
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.72.0
|
data/lib/persevere_adapter.rb
CHANGED
@@ -2,789 +2,24 @@ require 'rubygems'
|
|
2
2
|
require 'dm-core'
|
3
3
|
require 'dm-aggregates'
|
4
4
|
require 'dm-types'
|
5
|
-
require '
|
5
|
+
require 'dm-migrations'
|
6
|
+
require 'dm-migrations/auto_migration'
|
7
|
+
require 'dm-validations'
|
6
8
|
require 'bigdecimal'
|
7
9
|
require 'digest/md5'
|
8
10
|
|
9
|
-
#
|
10
|
-
require '
|
11
|
-
require 'dm/associations/relationship'
|
12
|
-
require 'dm/model'
|
13
|
-
require 'dm/property'
|
14
|
-
require 'dm/query'
|
15
|
-
require 'dm/resource'
|
11
|
+
# Require Persevere http client
|
12
|
+
require 'persevere_client'
|
16
13
|
|
17
|
-
|
14
|
+
# Require in Adapter modules
|
15
|
+
require 'persevere_adapter/query'
|
18
16
|
|
19
|
-
|
20
|
-
# Override BigDecimal to_json because it's ugly and doesn't work for us
|
21
|
-
#
|
22
|
-
class BigDecimal
|
23
|
-
alias to_json_old to_json
|
24
|
-
|
25
|
-
def to_json
|
26
|
-
to_s
|
27
|
-
end
|
28
|
-
end
|
17
|
+
require 'persevere_adapter/support/big_decimal'
|
29
18
|
|
30
|
-
|
31
|
-
|
32
|
-
module PersevereAdapter
|
33
|
-
def aggregate(query)
|
34
|
-
records = []
|
35
|
-
fields = query.fields
|
36
|
-
field_size = fields.size
|
37
|
-
|
38
|
-
connect if @persevere.nil?
|
39
|
-
resources = Array.new
|
19
|
+
require 'persevere_adapter/json_support'
|
20
|
+
require 'persevere_adapter/enhance'
|
40
21
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
response = @persevere.retrieve(path, headers)
|
22
|
+
require 'persevere_adapter/adapter'
|
23
|
+
require 'persevere_adapter/migrations'
|
24
|
+
require 'persevere_adapter/aggregates'
|
45
25
|
|
46
|
-
if response.code == "200"
|
47
|
-
results = [response.body]
|
48
|
-
results.each do |row_of_results|
|
49
|
-
row = query.fields.zip([row_of_results].flatten).map do |field, value|
|
50
|
-
if field.respond_to?(:operator)
|
51
|
-
send(field.operator, field.target, value)
|
52
|
-
else
|
53
|
-
field.typecast(value)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
records << (field_size > 1 ? row : row[0])
|
57
|
-
end
|
58
|
-
end
|
59
|
-
records
|
60
|
-
end # aggregate method
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
def count(property, value)
|
65
|
-
value.to_i
|
66
|
-
end
|
67
|
-
|
68
|
-
def min(property, value)
|
69
|
-
values = JSON.parse("[#{value}]").flatten.compact
|
70
|
-
if values.is_a?(Array)
|
71
|
-
values.map! { |v| property.typecast(v) }
|
72
|
-
return values.sort[0].new_offset(Rational(Time.now.getlocal.gmt_offset/3600, 24)) if property.type == DateTime
|
73
|
-
return values.sort[0]
|
74
|
-
end
|
75
|
-
property.typecast(value)
|
76
|
-
end
|
77
|
-
|
78
|
-
def max(property, value)
|
79
|
-
values = JSON.parse("[#{value}]").flatten.compact
|
80
|
-
if values.is_a?(Array)
|
81
|
-
values.map! { |v| property.typecast(v) }
|
82
|
-
return values.sort[-1].new_offset(Rational(Time.now.getlocal.gmt_offset/3600, 24)) if property.type == DateTime
|
83
|
-
return values.sort[-1]
|
84
|
-
end
|
85
|
-
property.typecast(value)
|
86
|
-
end
|
87
|
-
|
88
|
-
def avg(property, value)
|
89
|
-
values = JSON.parse(value).compact
|
90
|
-
result = values.inject(0.0){|sum,i| sum+=i }/values.length
|
91
|
-
property.type == Integer ? result.to_f : property.typecast(result)
|
92
|
-
end
|
93
|
-
|
94
|
-
def sum(property, value)
|
95
|
-
property.typecast(value)
|
96
|
-
end
|
97
|
-
end # module PersevereAdapter
|
98
|
-
end # module Aggregates
|
99
|
-
|
100
|
-
module Migrations
|
101
|
-
module PersevereAdapter
|
102
|
-
# @api private
|
103
|
-
def self.included(base)
|
104
|
-
DataMapper.extend(Migrations::SingletonMethods)
|
105
|
-
|
106
|
-
[ :Repository, :Model ].each do |name|
|
107
|
-
DataMapper.const_get(name).send(:include, Migrations.const_get(name))
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
# Returns whether the storage_name exists.
|
112
|
-
#
|
113
|
-
# @param [String] storage_name
|
114
|
-
# a String defining the name of a storage, for example a table name.
|
115
|
-
#
|
116
|
-
# @return [Boolean]
|
117
|
-
# true if the storage exists
|
118
|
-
#
|
119
|
-
# @api semipublic
|
120
|
-
def storage_exists?(storage_name)
|
121
|
-
class_names = JSON.parse(@persevere.retrieve('/Class/[=id]').body)
|
122
|
-
return true if class_names.include?("Class/"+storage_name)
|
123
|
-
false
|
124
|
-
end
|
125
|
-
|
126
|
-
##
|
127
|
-
# Creates the persevere schema from the model.
|
128
|
-
#
|
129
|
-
# @param [DataMapper::Model] model
|
130
|
-
# The model that corresponds to the storage schema that needs to be created.
|
131
|
-
#
|
132
|
-
# @api semipublic
|
133
|
-
def create_model_storage(model)
|
134
|
-
name = self.name
|
135
|
-
properties = model.properties_with_subclasses(name)
|
136
|
-
|
137
|
-
return false if storage_exists?(model.storage_name(name))
|
138
|
-
return false if properties.empty?
|
139
|
-
|
140
|
-
# Make sure storage for referenced objects exists
|
141
|
-
model.relationships.each_pair do |n, r|
|
142
|
-
if ! storage_exists?(r.child_model.storage_name)
|
143
|
-
put_schema({'id' => r.child_model.storage_name, 'properties' => {}})
|
144
|
-
end
|
145
|
-
end
|
146
|
-
schema_hash = model.to_json_schema_hash()
|
147
|
-
|
148
|
-
return true unless put_schema(schema_hash) == false
|
149
|
-
false
|
150
|
-
end
|
151
|
-
|
152
|
-
##
|
153
|
-
# Updates the persevere schema from the model.
|
154
|
-
#
|
155
|
-
# @param [DataMapper::Model] model
|
156
|
-
# The model that corresponds to the storage schema that needs to be updated.
|
157
|
-
#
|
158
|
-
# @api semipublic
|
159
|
-
def upgrade_model_storage(model)
|
160
|
-
name = self.name
|
161
|
-
properties = model.properties_with_subclasses(name)
|
162
|
-
|
163
|
-
DataMapper.logger.debug("Upgrading #{model.name}")
|
164
|
-
|
165
|
-
if success = create_model_storage(model)
|
166
|
-
return properties
|
167
|
-
end
|
168
|
-
|
169
|
-
new_schema_hash = model.to_json_schema_hash()
|
170
|
-
current_schema_hash = get_schema(new_schema_hash['id'])[0]
|
171
|
-
# TODO: Diff of what is there and what will be added.
|
172
|
-
|
173
|
-
new_properties = properties.map do |property|
|
174
|
-
prop_name = property.name.to_s
|
175
|
-
prop_type = property.type
|
176
|
-
next if prop_name == 'id' ||
|
177
|
-
(current_schema_hash['properties'].has_key?(prop_name) &&
|
178
|
-
new_schema_hash['properties'][prop_name]['type'] == current_schema_hash['properties'][prop_name]['type'] )
|
179
|
-
property
|
180
|
-
end.compact
|
181
|
-
|
182
|
-
return new_properties unless update_schema(new_schema_hash) == false
|
183
|
-
return nil
|
184
|
-
end
|
185
|
-
|
186
|
-
##
|
187
|
-
# Destroys the persevere schema from the model.
|
188
|
-
#
|
189
|
-
# @param [DataMapper::Model] model
|
190
|
-
# The model that corresponds to the storage schema that needs to be destroyed.
|
191
|
-
#
|
192
|
-
# @api semipublic
|
193
|
-
def destroy_model_storage(model)
|
194
|
-
return true unless storage_exists?(model.storage_name(name))
|
195
|
-
schema_hash = model.to_json_schema_hash()
|
196
|
-
return true unless delete_schema(schema_hash) == false
|
197
|
-
false
|
198
|
-
end
|
199
|
-
|
200
|
-
end # module PersevereAdapter
|
201
|
-
end # module Migrations
|
202
|
-
|
203
|
-
module Adapters
|
204
|
-
class PersevereAdapter < AbstractAdapter
|
205
|
-
extend Chainable
|
206
|
-
extend Deprecate
|
207
|
-
|
208
|
-
RESERVED_CLASSNAMES = ['User','Transaction','Capability','File','Class', 'Object', 'Versioned']
|
209
|
-
|
210
|
-
include Migrations::PersevereAdapter
|
211
|
-
|
212
|
-
# Default types for all data object based adapters.
|
213
|
-
#
|
214
|
-
# @return [Hash] default types for data objects adapters.
|
215
|
-
#
|
216
|
-
# @api private
|
217
|
-
chainable do
|
218
|
-
def type_map
|
219
|
-
length = Property::DEFAULT_LENGTH
|
220
|
-
precision = Property::DEFAULT_PRECISION
|
221
|
-
scale = Property::DEFAULT_SCALE_BIGDECIMAL
|
222
|
-
|
223
|
-
@type_map ||= {
|
224
|
-
Types::Serial => { :primitive => 'integer' },
|
225
|
-
Types::Boolean => { :primitive => 'boolean' },
|
226
|
-
Integer => { :primitive => 'integer'},
|
227
|
-
String => { :primitive => 'string'},
|
228
|
-
Class => { :primitive => 'string'},
|
229
|
-
BigDecimal => { :primitive => 'number'},
|
230
|
-
Float => { :primitive => 'number'},
|
231
|
-
DateTime => { :primitive => 'string', :format => 'date-time'},
|
232
|
-
Date => { :primitive => 'string', :format => 'date'},
|
233
|
-
Time => { :primitive => 'string', :format => 'time'},
|
234
|
-
TrueClass => { :primitive => 'boolean'},
|
235
|
-
Types::Text => { :primitive => 'string'},
|
236
|
-
DataMapper::Types::Object => { :primitive => 'string'},
|
237
|
-
DataMapper::Types::URI => { :primitive => 'string', :format => 'uri'}
|
238
|
-
}.freeze
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# This should go away when we have more methods exposed to retrieve versioned data (and schemas)
|
243
|
-
attr_accessor :persevere
|
244
|
-
|
245
|
-
##
|
246
|
-
# Used by DataMapper to put records into a data-store: "INSERT"
|
247
|
-
# in SQL-speak. It takes an array of the resources (model
|
248
|
-
# instances) to be saved. Resources each have a key that can be
|
249
|
-
# used to quickly look them up later without searching, if the
|
250
|
-
# adapter supports it.
|
251
|
-
#
|
252
|
-
# @param [Array<DataMapper::Resource>] resources
|
253
|
-
# The set of resources (model instances)
|
254
|
-
#
|
255
|
-
# @return [Integer]
|
256
|
-
# The number of records that were actually saved into the
|
257
|
-
# data-store
|
258
|
-
#
|
259
|
-
# @api semipublic
|
260
|
-
def create(resources)
|
261
|
-
connect if @persevere.nil?
|
262
|
-
created = 0
|
263
|
-
|
264
|
-
check_schemas
|
265
|
-
|
266
|
-
resources.each do |resource|
|
267
|
-
serial = resource.model.serial(self.name)
|
268
|
-
path = "/#{resource.model.storage_name}/"
|
269
|
-
# Invoke to_json_hash with a boolean to indicate this is a create
|
270
|
-
# We might want to make this a post-to_json_hash cleanup instead
|
271
|
-
payload = resource.to_json_hash(false).delete_if{|key,value| value.nil? }
|
272
|
-
DataMapper.logger.debug("--> PATH/PAYLOAD: #{path} #{payload.inspect}")
|
273
|
-
response = @persevere.create(path, payload)
|
274
|
-
|
275
|
-
# Check the response, this needs to be more robust and raise
|
276
|
-
# exceptions when there's a problem
|
277
|
-
if response.code == "201"# good:
|
278
|
-
rsrc_hash = JSON.parse(response.body)
|
279
|
-
# Typecast attributes, DM expects them properly cast
|
280
|
-
resource.model.properties.each do |prop|
|
281
|
-
value = rsrc_hash[prop.field.to_s]
|
282
|
-
rsrc_hash[prop.field.to_s] = prop.typecast(value) unless value.nil?
|
283
|
-
# Shift date/time objects to the correct timezone because persevere is UTC
|
284
|
-
case prop
|
285
|
-
when DateTime then rsrc_hash[prop.field.to_s] = value.new_offset(Rational(Time.now.getlocal.gmt_offset/3600, 24))
|
286
|
-
when Time then rsrc_hash[prop.field.to_s] = value.getlocal
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
serial.set!(resource, rsrc_hash["id"]) unless serial.nil?
|
291
|
-
|
292
|
-
created += 1
|
293
|
-
else
|
294
|
-
return false
|
295
|
-
end
|
296
|
-
end
|
297
|
-
|
298
|
-
# Return the number of resources created in persevere.
|
299
|
-
return created
|
300
|
-
end
|
301
|
-
|
302
|
-
##
|
303
|
-
# Used by DataMapper to update the attributes on existing
|
304
|
-
# records in a data-store: "UPDATE" in SQL-speak. It takes a
|
305
|
-
# hash of the attributes to update with, as well as a query
|
306
|
-
# object that specifies which resources should be updated.
|
307
|
-
#
|
308
|
-
# @param [Hash] attributes
|
309
|
-
# A set of key-value pairs of the attributes to update the
|
310
|
-
# resources with.
|
311
|
-
# @param [DataMapper::Query] query
|
312
|
-
# The query that should be used to find the resource(s) to
|
313
|
-
# update.
|
314
|
-
#
|
315
|
-
# @return [Integer]
|
316
|
-
# the number of records that were successfully updated
|
317
|
-
#
|
318
|
-
# @api semipublic
|
319
|
-
def update(attributes, query)
|
320
|
-
connect if @persevere.nil?
|
321
|
-
updated = 0
|
322
|
-
|
323
|
-
check_schemas
|
324
|
-
|
325
|
-
if ! query.is_a?(DataMapper::Query)
|
326
|
-
resources = [query].flatten
|
327
|
-
else
|
328
|
-
resources = read_many(query)
|
329
|
-
end
|
330
|
-
|
331
|
-
resources.each do |resource|
|
332
|
-
tblname = resource.model.storage_name
|
333
|
-
path = "/#{tblname}/#{resource.key.first}"
|
334
|
-
payload = resource.to_json_hash()
|
335
|
-
DataMapper.logger.debug("--> PATH/PAYLOAD: #{path} #{payload.inspect}")
|
336
|
-
result = @persevere.update(path, payload)
|
337
|
-
|
338
|
-
if result.code == "200"
|
339
|
-
updated += 1
|
340
|
-
else
|
341
|
-
return false
|
342
|
-
end
|
343
|
-
end
|
344
|
-
return updated
|
345
|
-
end
|
346
|
-
|
347
|
-
##
|
348
|
-
# Look up a single record from the data-store. "SELECT ... LIMIT
|
349
|
-
# 1" in SQL. Used by Model#get to find a record by its
|
350
|
-
# identifier(s), and Model#first to find a single record by some
|
351
|
-
# search query.
|
352
|
-
#
|
353
|
-
# @param [DataMapper::Query] query
|
354
|
-
# The query to be used to locate the resource.
|
355
|
-
#
|
356
|
-
# @return [DataMapper::Resource]
|
357
|
-
# A Resource object representing the record that was found, or
|
358
|
-
# nil for no matching records.
|
359
|
-
#
|
360
|
-
# @api semipublic
|
361
|
-
|
362
|
-
def read_one(query)
|
363
|
-
# TODO: This would be more efficient if it modified the query to limit = 1,
|
364
|
-
# rather than getting all of them and only returning the first one.
|
365
|
-
results = read_many(query)
|
366
|
-
results[0,1]
|
367
|
-
end
|
368
|
-
|
369
|
-
##
|
370
|
-
# Looks up a collection of records from the data-store: "SELECT"
|
371
|
-
# in SQL. Used by Model#all to search for a set of records;
|
372
|
-
# that set is in a DataMapper::Collection object.
|
373
|
-
#
|
374
|
-
# @param [DataMapper::Query] query
|
375
|
-
# The query to be used to seach for the resources
|
376
|
-
#
|
377
|
-
# @return [DataMapper::Collection]
|
378
|
-
# A collection of all the resources found by the query.
|
379
|
-
#
|
380
|
-
# @api semipublic
|
381
|
-
def read_many(query)
|
382
|
-
connect if @persevere.nil?
|
383
|
-
|
384
|
-
resources = Array.new
|
385
|
-
tblname = query.model.storage_name
|
386
|
-
|
387
|
-
json_query, headers = query.to_json_query
|
388
|
-
|
389
|
-
path = "/#{tblname}/#{json_query}"
|
390
|
-
DataMapper.logger.debug("--> PATH/QUERY: #{path}")
|
391
|
-
|
392
|
-
response = @persevere.retrieve(path, headers)
|
393
|
-
|
394
|
-
if response.code.match(/20?/)
|
395
|
-
results = JSON.parse(response.body)
|
396
|
-
results.each do |rsrc_hash|
|
397
|
-
# Typecast attributes, DM expects them properly cast
|
398
|
-
query.fields.each do |prop|
|
399
|
-
object_reference = false
|
400
|
-
pname = prop.field.to_s
|
401
|
-
if pname[-3,3] == "_id"
|
402
|
-
pname = pname[0..-4]
|
403
|
-
object_reference = true
|
404
|
-
end
|
405
|
-
value = rsrc_hash[pname]
|
406
|
-
# Dereference references
|
407
|
-
unless value.nil?
|
408
|
-
if value.is_a?(Hash)
|
409
|
-
if value.has_key?("$ref")
|
410
|
-
value = value["$ref"].split("/")[-1]
|
411
|
-
end
|
412
|
-
elsif value.is_a?(Array)
|
413
|
-
value = value.map do |v|
|
414
|
-
if v.has_key?("$ref")
|
415
|
-
v = v["$ref"].split("/")[-1]
|
416
|
-
else
|
417
|
-
v
|
418
|
-
end
|
419
|
-
end
|
420
|
-
end
|
421
|
-
if prop.field == 'id'
|
422
|
-
rsrc_hash[pname] = prop.typecast(value.to_s.match(/(#{tblname})?\/?([a-zA-Z0-9_-]+$)/)[2])
|
423
|
-
else
|
424
|
-
rsrc_hash[pname] = prop.typecast(value)
|
425
|
-
end
|
426
|
-
end
|
427
|
-
# Shift date/time objects to the correct timezone because persevere is UTC
|
428
|
-
case prop
|
429
|
-
when DateTime then rsrc_hash[pname] = value.new_offset(Rational(Time.now.getlocal.gmt_offset/3600, 24))
|
430
|
-
when Time then rsrc_hash[pname] = value.getlocal
|
431
|
-
end
|
432
|
-
end
|
433
|
-
end
|
434
|
-
resources = query.model.load(results, query)
|
435
|
-
end
|
436
|
-
# We could almost elimate this if regexp was working in persevere.
|
437
|
-
|
438
|
-
# This won't work if the RegExp is nested more then 1 layer deep.
|
439
|
-
if query.conditions.class == DataMapper::Query::Conditions::AndOperation
|
440
|
-
regexp_conds = query.conditions.operands.select do |obj|
|
441
|
-
obj.is_a?(DataMapper::Query::Conditions::RegexpComparison) ||
|
442
|
-
( obj.is_a?(DataMapper::Query::Conditions::NotOperation) && obj.operand.is_a?(DataMapper::Query::Conditions::RegexpComparison) )
|
443
|
-
end
|
444
|
-
regexp_conds.each{|cond| resources = resources.select{|resource| cond.matches?(resource)} }
|
445
|
-
|
446
|
-
end
|
447
|
-
|
448
|
-
# query.match_records(resources)
|
449
|
-
resources
|
450
|
-
end
|
451
|
-
|
452
|
-
alias :read :read_many
|
453
|
-
|
454
|
-
##
|
455
|
-
# Destroys all the records matching the given query. "DELETE" in SQL.
|
456
|
-
#
|
457
|
-
# @param [DataMapper::Query] query
|
458
|
-
# The query used to locate the resources to be deleted.
|
459
|
-
#
|
460
|
-
# @return [Integer]
|
461
|
-
# The number of records that were deleted.
|
462
|
-
#
|
463
|
-
# @api semipublic
|
464
|
-
def delete(query)
|
465
|
-
connect if @persevere.nil?
|
466
|
-
|
467
|
-
deleted = 0
|
468
|
-
|
469
|
-
if ! query.is_a?(DataMapper::Query)
|
470
|
-
resources = [query].flatten
|
471
|
-
else
|
472
|
-
resources = read_many(query)
|
473
|
-
end
|
474
|
-
|
475
|
-
resources.each do |resource|
|
476
|
-
tblname = resource.model.storage_name
|
477
|
-
path = "/#{tblname}/#{resource.id}"
|
478
|
-
|
479
|
-
result = @persevere.delete(path)
|
480
|
-
|
481
|
-
if result.code == "204" # ok
|
482
|
-
deleted += 1
|
483
|
-
end
|
484
|
-
end
|
485
|
-
return deleted
|
486
|
-
end
|
487
|
-
|
488
|
-
##
|
489
|
-
#
|
490
|
-
# Other methods for the Yogo Data Management Toolkit
|
491
|
-
#
|
492
|
-
##
|
493
|
-
def get_schema(name = nil, project = nil)
|
494
|
-
path = nil
|
495
|
-
single = false
|
496
|
-
|
497
|
-
if name.nil? & project.nil?
|
498
|
-
path = "/Class/"
|
499
|
-
elsif project.nil?
|
500
|
-
path = "/Class/#{name}"
|
501
|
-
elsif name.nil?
|
502
|
-
path = "/Class/#{project}/"
|
503
|
-
else
|
504
|
-
path = "/Class/#{project}/#{name}"
|
505
|
-
end
|
506
|
-
result = @persevere.retrieve(path)
|
507
|
-
if result.code == "200"
|
508
|
-
schemas = [JSON.parse(result.body)].flatten.select{ |schema| not RESERVED_CLASSNAMES.include?(schema['id']) }
|
509
|
-
schemas.each do |schema|
|
510
|
-
if schema.has_key?('properties')
|
511
|
-
schema['properties']['id'] = { 'type' => "serial", 'index' => true }
|
512
|
-
end
|
513
|
-
end
|
514
|
-
|
515
|
-
return name.nil? ? schemas : schemas[0..0]
|
516
|
-
else
|
517
|
-
return false
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
##
|
522
|
-
#
|
523
|
-
def put_schema(schema_hash, project = nil)
|
524
|
-
path = "/Class/"
|
525
|
-
if ! project.nil?
|
526
|
-
if schema_hash.has_key?("id")
|
527
|
-
if ! schema_hash['id'].index(project)
|
528
|
-
schema_hash['id'] = "#{project}/#{schema_hash['id']}"
|
529
|
-
end
|
530
|
-
else
|
531
|
-
DataMapper.logger.error("You need an id key/value in the hash")
|
532
|
-
end
|
533
|
-
end
|
534
|
-
|
535
|
-
scrub_schema(schema_hash['properties'])
|
536
|
-
properties = schema_hash.delete('properties')
|
537
|
-
schema_hash['extends'] = { "$ref" => "/Class/Versioned" } if @options[:versioned]
|
538
|
-
schema_hash.delete_if{|key,value| value.nil? }
|
539
|
-
result = @persevere.create(path, schema_hash)
|
540
|
-
if result.code == '201'
|
541
|
-
# return JSON.parse(result.body)
|
542
|
-
schema_hash['properties'] = properties
|
543
|
-
return update_schema(schema_hash)
|
544
|
-
else
|
545
|
-
return false
|
546
|
-
end
|
547
|
-
end
|
548
|
-
|
549
|
-
##
|
550
|
-
#
|
551
|
-
def update_schema(schema_hash, project = nil)
|
552
|
-
id = schema_hash['id']
|
553
|
-
payload = schema_hash.reject{|key,value| key.to_sym.eql?(:id) }
|
554
|
-
scrub_schema(payload['properties'])
|
555
|
-
payload['extends'] = { "$ref" => "/Class/Versioned" } if @options[:versioned]
|
556
|
-
|
557
|
-
if project.nil?
|
558
|
-
path = "/Class/#{id}"
|
559
|
-
else
|
560
|
-
path = "/Class/#{project}/#{id}"
|
561
|
-
end
|
562
|
-
|
563
|
-
result = @persevere.update(path, payload)
|
564
|
-
|
565
|
-
if result.code == '200'
|
566
|
-
return result.body
|
567
|
-
else
|
568
|
-
return false
|
569
|
-
end
|
570
|
-
end
|
571
|
-
|
572
|
-
##
|
573
|
-
#
|
574
|
-
def delete_schema(schema_hash, project = nil)
|
575
|
-
if ! project.nil?
|
576
|
-
if schema_hash.has_key?("id")
|
577
|
-
if ! schema_hash['id'].index(project)
|
578
|
-
schema_hash['id'] = "#{project}/#{schema_hash['id']}"
|
579
|
-
end
|
580
|
-
else
|
581
|
-
DataMapper.logger.error("You need an id key/value in the hash")
|
582
|
-
end
|
583
|
-
end
|
584
|
-
|
585
|
-
path = "/Class/#{schema_hash['id']}"
|
586
|
-
result = @persevere.delete(path)
|
587
|
-
|
588
|
-
if result.code == "204"
|
589
|
-
return true
|
590
|
-
else
|
591
|
-
return false
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
private
|
596
|
-
|
597
|
-
##
|
598
|
-
# Make a new instance of the adapter. The @model_records ivar is
|
599
|
-
# the 'data-store' for this adapter. It is not shared amongst
|
600
|
-
# multiple incarnations of this adapter, eg
|
601
|
-
# DataMapper.setup(:default, :adapter => :in_memory);
|
602
|
-
# DataMapper.setup(:alternate, :adapter => :in_memory) do not
|
603
|
-
# share the data-store between them.
|
604
|
-
#
|
605
|
-
# @param [String, Symbol] name
|
606
|
-
# The name of the DataMapper::Repository using this adapter.
|
607
|
-
# @param [String, Hash] uri_or_options
|
608
|
-
# The connection uri string, or a hash of options to set up
|
609
|
-
# the adapter
|
610
|
-
#
|
611
|
-
# @api semipublic
|
612
|
-
def initialize(name, uri_or_options)
|
613
|
-
super
|
614
|
-
|
615
|
-
if uri_or_options.class
|
616
|
-
@identity_maps = {}
|
617
|
-
end
|
618
|
-
|
619
|
-
@options = Hash.new
|
620
|
-
|
621
|
-
uri_or_options.each do |k,v|
|
622
|
-
@options[k.to_sym] = v
|
623
|
-
end
|
624
|
-
|
625
|
-
@options[:scheme] = @options[:adapter]
|
626
|
-
@options.delete(:scheme)
|
627
|
-
|
628
|
-
# @resource_naming_convention = NamingConventions::Resource::Underscored
|
629
|
-
@resource_naming_convention = lambda do |value|
|
630
|
-
# value.split('::').map{ |val| Extlib::Inflection.underscore(val) }.join('__')
|
631
|
-
Extlib::Inflection.underscore(value).gsub('/', '__')
|
632
|
-
end
|
633
|
-
|
634
|
-
@identity_maps = {}
|
635
|
-
@persevere = nil
|
636
|
-
@prepped = false
|
637
|
-
@schema_backups = Array.new
|
638
|
-
@last_backup = nil
|
639
|
-
|
640
|
-
connect
|
641
|
-
end
|
642
|
-
|
643
|
-
private
|
644
|
-
|
645
|
-
##
|
646
|
-
#
|
647
|
-
def connect
|
648
|
-
if ! @prepped
|
649
|
-
uri = URI::HTTP.build(@options).to_s
|
650
|
-
@persevere = Persevere.new(uri)
|
651
|
-
prep_persvr unless @prepped
|
652
|
-
end
|
653
|
-
end
|
654
|
-
|
655
|
-
def scrub_data(json_hash)
|
656
|
-
items = [DataMapper::Model.descendants.map{|c| "#{c.name.downcase}_id"}].flatten
|
657
|
-
items.each { |item| json_hash.delete(item) if json_hash.has_key?(item) }
|
658
|
-
json_hash.reject! { |k,v| v.nil? }
|
659
|
-
json_hash
|
660
|
-
end
|
661
|
-
|
662
|
-
##
|
663
|
-
#
|
664
|
-
def scrub_schema(json_hash)
|
665
|
-
items = [DataMapper::Model.descendants.map{|c| "#{c.name.downcase}_id"}, 'id'].flatten
|
666
|
-
items.each { |item| json_hash.delete(item) if json_hash.has_key?(item) }
|
667
|
-
json_hash
|
668
|
-
end
|
669
|
-
|
670
|
-
def check_schemas
|
671
|
-
schemas = @persevere.retrieve("/Class").body
|
672
|
-
md5 = Digest::MD5.hexdigest(schemas)
|
673
|
-
|
674
|
-
if ! @last_backup.nil?
|
675
|
-
if @last_backup[:hash] != md5
|
676
|
-
DataMapper.logger.debug("Schemas changed, do you know why? (#{md5} :: #{@last_backup[:hash]})")
|
677
|
-
@schema_backups.each do |sb|
|
678
|
-
if sb[:hash] == md5
|
679
|
-
DataMapper.logger.debug("Schemas reverted to #{sb.inspect}")
|
680
|
-
end
|
681
|
-
end
|
682
|
-
end
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
def save_schemas
|
687
|
-
schemas = @persevere.retrieve("/Class").body
|
688
|
-
md5 = Digest::MD5.hexdigest(schemas)
|
689
|
-
@last_backup = { :hash => md5, :schemas => schemas, :timestamp => Time.now }
|
690
|
-
@schema_backups << @last_backup
|
691
|
-
# Dump to filesystem
|
692
|
-
end
|
693
|
-
|
694
|
-
def get_classes
|
695
|
-
# Because this is an AbstractAdapter and not a
|
696
|
-
# DataObjectAdapter, we can't assume there are any schemas
|
697
|
-
# present, so we retrieve the ones that exist and keep them up
|
698
|
-
# to date
|
699
|
-
classes = Array.new
|
700
|
-
result = @persevere.retrieve('/Class[=id]')
|
701
|
-
if result.code == "200"
|
702
|
-
hresult = JSON.parse(result.body)
|
703
|
-
hresult.each do |cname|
|
704
|
-
junk,name = cname.split("/")
|
705
|
-
classes << name
|
706
|
-
end
|
707
|
-
else
|
708
|
-
DataMapper.logger.error("Error retrieving existing tables: #{result}")
|
709
|
-
end
|
710
|
-
classes
|
711
|
-
end
|
712
|
-
|
713
|
-
##
|
714
|
-
#
|
715
|
-
def prep_persvr
|
716
|
-
#
|
717
|
-
# If the user specified a versioned datastore load the versioning REST code
|
718
|
-
#
|
719
|
-
unless get_classes.include?("Versioned") && @options[:versioned]
|
720
|
-
versioned_class =<<-EOF
|
721
|
-
{
|
722
|
-
id: "Versioned",
|
723
|
-
prototype: {
|
724
|
-
getVersionMethod: function() {
|
725
|
-
return java.lang.Class.forName("org.persvr.data.Persistable").getMethod("getVersion");
|
726
|
-
},
|
727
|
-
isCurrentVersion: function() {
|
728
|
-
return this.getVersionMethod().invoke(this).isCurrent();
|
729
|
-
},
|
730
|
-
getVersionNumber: function() {
|
731
|
-
return this.getVersionMethod().invoke(this).getVersionNumber();
|
732
|
-
},
|
733
|
-
getPrevious: function() {
|
734
|
-
var prev = this.getVersionMethod().invoke(this).getPreviousVersion();
|
735
|
-
return prev;
|
736
|
-
},
|
737
|
-
getAllPrevious: function() {
|
738
|
-
|
739
|
-
var current = this;
|
740
|
-
var prev = current && current.getPrevious();
|
741
|
-
|
742
|
-
var versions = []
|
743
|
-
while(current && prev) {
|
744
|
-
versions.push(prev);
|
745
|
-
current = prev;
|
746
|
-
prev = current.getPrevious();
|
747
|
-
}
|
748
|
-
|
749
|
-
return versions;
|
750
|
-
},
|
751
|
-
"representation:application/json+versioned": {
|
752
|
-
quality: 0.2,
|
753
|
-
output: function(object) {
|
754
|
-
var previous = object.getAllPrevious();
|
755
|
-
response.setContentType("application/json+versioned");
|
756
|
-
response.getOutputStream().print(JSON.stringify({
|
757
|
-
version: object.getVersionNumber(),
|
758
|
-
current: object,
|
759
|
-
versions: previous
|
760
|
-
}));
|
761
|
-
}
|
762
|
-
}
|
763
|
-
}
|
764
|
-
}
|
765
|
-
EOF
|
766
|
-
|
767
|
-
response = @persevere.create('/Class/', versioned_class, { 'Content-Type' => 'application/javascript' } )
|
768
|
-
|
769
|
-
# Check the response, this needs to be more robust and raise
|
770
|
-
# exceptions when there's a problem
|
771
|
-
if response.code == "201"# good:
|
772
|
-
DataMapper.logger.info("Created versioned class.")
|
773
|
-
else
|
774
|
-
DataMapper.logger.info("Failed to create versioned class.")
|
775
|
-
end
|
776
|
-
|
777
|
-
# headers = { 'Content-Type' => 'application/javascript', 'Accept' => 'application/json' }
|
778
|
-
# begin
|
779
|
-
# puts "POST #{URI.encode('/Class')}, #{versioned_class}, #{headers.inspect}"
|
780
|
-
# response = @persevere.persevere.send_request('POST', URI.encode('/Class/'), versioned_class, headers )
|
781
|
-
# rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
782
|
-
# Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
783
|
-
# DataMapper.logger.error("Persevere Create Failed: #{e}, Trying again.")
|
784
|
-
# end
|
785
|
-
end
|
786
|
-
end
|
787
|
-
end # class PersevereAdapter
|
788
|
-
const_added(:PersevereAdapter)
|
789
|
-
end # module Adapters
|
790
|
-
end # module DataMapper
|