tinkit 0.0.0
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.
- data/LICENSE +176 -0
- data/README +11 -0
- data/Rakefile +75 -0
- data/lib/glue_envs/couchrest/couchrest_attachment_handler.rb +260 -0
- data/lib/glue_envs/couchrest/couchrest_files_mgr.rb +198 -0
- data/lib/glue_envs/couchrest_glue_env.rb +536 -0
- data/lib/glue_envs/files_mgr_base.rb +51 -0
- data/lib/glue_envs/filesystem/filesystem_files_mgr.rb +187 -0
- data/lib/glue_envs/filesystem_glue_env.rb +395 -0
- data/lib/glue_envs/mysql/mysql_files_mgr.rb +175 -0
- data/lib/glue_envs/mysql_glue_env.rb +428 -0
- data/lib/glue_envs/sdb_s3/sdb_s3_files_mgr.rb +314 -0
- data/lib/glue_envs/sdb_s3_glue_env.rb +248 -0
- data/lib/helpers/camel.rb +21 -0
- data/lib/helpers/filesystem_helpers.rb +27 -0
- data/lib/helpers/hash_helpers.rb +74 -0
- data/lib/helpers/log_helper.rb +34 -0
- data/lib/helpers/mime_types_new.rb +126 -0
- data/lib/helpers/old_more_open_struct.rb +28 -0
- data/lib/helpers/require_helper.rb +45 -0
- data/lib/helpers/tk_escape.rb +17 -0
- data/lib/midas/bufs_data_structure.rb +84 -0
- data/lib/midas/node_element_operations.rb +264 -0
- data/lib/tinkit.rb +38 -0
- data/lib/tinkit_base_node.rb +733 -0
- data/lib/tinkit_node_factory.rb +47 -0
- data/spec/couchrest_files_mgr_spec.rb +551 -0
- data/spec/couchrest_glue_spec.rb +246 -0
- data/spec/filesystem_files_mgr_spec.rb +236 -0
- data/spec/filesystem_glue_spec.rb +243 -0
- data/spec/filesystem_helpers_spec.rb +42 -0
- data/spec/helpers/bufs_node_builder.rb +17 -0
- data/spec/helpers/bufs_sample_dataset.rb +160 -0
- data/spec/helpers/bufs_test_environments.rb +81 -0
- data/spec/helpers/tmp_view_cleaner.rb +15 -0
- data/spec/lib_helpers/tk_escape_spec.rb +45 -0
- data/spec/mysql_files_mgr_spec.rb +250 -0
- data/spec/mysql_glue_spec.rb +214 -0
- data/spec/node_element_operations_spec.rb +392 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec1.rb +82 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec2.rb +68 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec3.rb +80 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec4.rb +110 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec5.rb +84 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec6.rb +83 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec7.rb +101 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec8.rb +92 -0
- data/spec/sdb_s3_files_mgr_spec/sdb_s3_files_mgr_spec_all.rb +266 -0
- data/spec/sdb_s3_glue_spec.rb +230 -0
- data/spec/tinkit_node_factory_spec.rb +1108 -0
- metadata +114 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
#require helper for cleaner require statements
|
2
|
+
#require File.join(File.dirname(__FILE__), '../../helpers/require_helper')
|
3
|
+
|
4
|
+
#Tinkit directory organization defined in lib/helpers/require_helper
|
5
|
+
require Tinkit.glue 'couchrest/couchrest_attachment_handler'
|
6
|
+
require Tinkit.helpers 'hash_helpers'
|
7
|
+
|
8
|
+
module CouchrestInterface
|
9
|
+
class FilesMgr
|
10
|
+
|
11
|
+
attr_accessor :attachment_doc_class
|
12
|
+
|
13
|
+
#not tested in factory tests, tested in couchrest though
|
14
|
+
def self.get_att_doc(node)
|
15
|
+
node_id = node._model_metadata[:_id]
|
16
|
+
attachment_doc_id = node.my_GlueEnv.moab_data[:attachClass].uniq_att_doc_id(node_id)
|
17
|
+
att_doc = node.my_GlueEnv.moab_data[:db].get(attachment_doc_id)
|
18
|
+
if att_doc
|
19
|
+
return att_doc
|
20
|
+
else
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(node_env, node_key_value)
|
26
|
+
#for bufs node_key is the value of :my_category
|
27
|
+
#although it is not used in this class, it is required to
|
28
|
+
#maintain consitency with tinkit_base_node
|
29
|
+
#TODO: Actually the goal is for moab's to have no dependency on tinkit_base_node
|
30
|
+
#so maybe the glue environment should have a files interface to tinkit_base_node??
|
31
|
+
#@attachment_doc_class = node_env.attachClass #old
|
32
|
+
#TODO: just pass moab_data??
|
33
|
+
@attachment_doc_class = node_env.moab_data[:attachClass]
|
34
|
+
end
|
35
|
+
|
36
|
+
def add(node, file_datas)
|
37
|
+
bia_class = @attachment_doc_class
|
38
|
+
attachment_package = {}
|
39
|
+
file_datas = [file_datas].flatten
|
40
|
+
file_datas.each do |file_data|
|
41
|
+
#get file data
|
42
|
+
src_filename = file_data[:src_filename]
|
43
|
+
src_basename = TkEscape.escape(File.basename(src_filename))
|
44
|
+
raise "File data must include the source filename when adding a file to the model" unless src_filename
|
45
|
+
model_basename = file_data[:model_basename] || src_basename
|
46
|
+
model_basename.gsub!('+', ' ') #plus signs are problematic
|
47
|
+
#TODO: Consider creating TkEscape.unescape method
|
48
|
+
model_basename = CGI.unescape(model_basename)
|
49
|
+
content_type = file_data[:content_type] || MimeNew.for_ofc_x(model_basename)
|
50
|
+
modified_time = file_data[:modified_time] || File.mtime(src_filename).to_s
|
51
|
+
#create attachment class data structure
|
52
|
+
file_metadata = {}
|
53
|
+
file_metadata['content_type'] = content_type
|
54
|
+
file_metadata['file_modified'] = modified_time
|
55
|
+
#read in file
|
56
|
+
#TODO: reading the file in this way is memory intensive for large files, chunking it up would be better
|
57
|
+
file_data = File.open(src_filename, "rb") {|f| f.read}
|
58
|
+
attachment_package[model_basename] = {'data' => file_data, 'md' => file_metadata}
|
59
|
+
end
|
60
|
+
#attachment package has now been created
|
61
|
+
#create the attachment record
|
62
|
+
#The attachment handler (bia_class) will deal with creating vs updating
|
63
|
+
user_id = node.my_GlueEnv.user_id
|
64
|
+
node_id = node._model_metadata[:_id]
|
65
|
+
#TODO: There is probably a cleaner way to do add attachments, but low on the priority list
|
66
|
+
record = bia_class.add_attachment_package(node_id, bia_class, attachment_package)
|
67
|
+
|
68
|
+
#get the basenames we just stored
|
69
|
+
stored_basenames = record['_attachments'].keys
|
70
|
+
if node.respond_to? :attachment_doc_id
|
71
|
+
#make sure the objects attachment id matches the persistence layer's record id
|
72
|
+
if node.attachment_doc_id && (node.attachment_doc_id != record['_id'] )
|
73
|
+
raise "Attachment ID mismatch, current id: #{node.attachment_doc_id} new id: #{record['_id']}"
|
74
|
+
#if the attachment id doesn't exist, create it
|
75
|
+
elsif node.attachment_doc_id.nil?
|
76
|
+
node.attachment_doc_id = record['_id'] #TODO is it nil after all attachs are deleted?
|
77
|
+
else
|
78
|
+
#we will reach here when everything is fine but we don't need to do anything
|
79
|
+
end
|
80
|
+
else #it's a new attachment and the attachment id has not been set, so we create and set it
|
81
|
+
node.__set_userdata_key(:attachment_doc_id, record['_id'] )
|
82
|
+
end
|
83
|
+
stored_basenames
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_raw_data(node, attach_name, content_type, raw_data, file_modified_at = nil)
|
87
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
88
|
+
file_metadata = {}
|
89
|
+
if file_modified_at
|
90
|
+
file_metadata['file_modified'] = file_modified_at
|
91
|
+
else
|
92
|
+
file_metadata['file_modified'] = Time.now.to_s
|
93
|
+
end
|
94
|
+
file_metadata['content_type'] = content_type
|
95
|
+
attachment_package = {}
|
96
|
+
unesc_attach_name = TkEscape.unescape(attach_name)
|
97
|
+
attachment_package[unesc_attach_name] = {'data' => raw_data, 'md' => file_metadata}
|
98
|
+
node_id = node._model_metadata[:_id]
|
99
|
+
record = bia_class.add_attachment_package(node_id, bia_class, attachment_package)
|
100
|
+
if node.respond_to? :attachment_doc_id
|
101
|
+
if node.attachment_doc_id && (node.attachment_doc_id != record['_id'] )
|
102
|
+
raise "Attachment ID mismatch, current id: #{node.attachment_doc_id} new id: #{record['_id']}"
|
103
|
+
elsif node.attachment_doc_id.nil?
|
104
|
+
node.attachment_doc_id = record['_id'] #TODO How is it nil?
|
105
|
+
end
|
106
|
+
else
|
107
|
+
node.__set_userdata_key(:attachment_doc_id, record['_id'] )
|
108
|
+
end
|
109
|
+
[attach_name]
|
110
|
+
end
|
111
|
+
|
112
|
+
#to conform with files_mgr
|
113
|
+
def subtract(node, file_basenames)
|
114
|
+
subtract_files(node, file_basenames)
|
115
|
+
end
|
116
|
+
|
117
|
+
#TODO Document the :all shortcut somewhere
|
118
|
+
def subtract_files(node, model_basenames)
|
119
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
120
|
+
if model_basenames == :all
|
121
|
+
subtract_all(node, bia_class)
|
122
|
+
else
|
123
|
+
subtract_some(node, model_basenames, bia_class)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_raw_data(node, model_basename)
|
128
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
129
|
+
node_id = node._model_metadata[:_id]
|
130
|
+
bia_doc_id = bia_class.uniq_att_doc_id(node_id)
|
131
|
+
bia_doc = bia_class.get(bia_doc_id)
|
132
|
+
return nil unless bia_doc
|
133
|
+
begin
|
134
|
+
rtn = bia_doc.fetch_attachment(model_basename)
|
135
|
+
rescue RestClient::ResourceNotFound
|
136
|
+
return nil
|
137
|
+
end
|
138
|
+
rtn
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_attachments_metadata(node)
|
142
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
143
|
+
node_id = node._model_metadata[:_id]
|
144
|
+
bia_doc_id = bia_class.uniq_att_doc_id(node_id)
|
145
|
+
bia_doc = bia_class.get(bia_doc_id)
|
146
|
+
atts = bia_doc.get_attachments
|
147
|
+
md_symified = {}
|
148
|
+
atts.each do |k,v|
|
149
|
+
v_symified = HashKeys.str_to_sym(v)
|
150
|
+
md_symified[k] = v_symified
|
151
|
+
end
|
152
|
+
md_symified
|
153
|
+
end
|
154
|
+
|
155
|
+
def list(node)
|
156
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
157
|
+
node_id = node._model_metadata[:_id]
|
158
|
+
bia_doc_id = bia_class.uniq_att_doc_id(node_id)
|
159
|
+
bia_doc = bia_class.get(bia_doc_id)
|
160
|
+
return nil unless bia_doc
|
161
|
+
bia_class.get_attachments(bia_doc).keys
|
162
|
+
end
|
163
|
+
|
164
|
+
#TODO: make private
|
165
|
+
def subtract_some(node, model_basenames, bia_class)
|
166
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
167
|
+
node_id = node._model_metadata[:_id]
|
168
|
+
bia_doc_id = bia_class.uniq_att_doc_id(node_id)
|
169
|
+
if bia_doc_id
|
170
|
+
bia_doc = bia_class.get(bia_doc_id)
|
171
|
+
raise "No attachment handler found for node: #{node.inspect}" unless bia_doc
|
172
|
+
bia_doc.remove_attachment(model_basenames)
|
173
|
+
rem_atts = bia_doc.get_attachments
|
174
|
+
subtract_all(node, bia_class) if rem_atts.empty?
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#TODO: Validate that we want to remove the files container
|
179
|
+
#TODO: does attachment_doc_id exist any more?
|
180
|
+
def subtract_all(node, bia_class)
|
181
|
+
#delete the attachment record
|
182
|
+
doc_db = node.my_GlueEnv.moab_data[:db]
|
183
|
+
|
184
|
+
bia_class = node.my_GlueEnv.moab_data[:attachClass]
|
185
|
+
node_id = node._model_metadata[:_id]
|
186
|
+
bia_doc_id = bia_class.uniq_att_doc_id(node_id)
|
187
|
+
if bia_doc_id
|
188
|
+
attach_doc = bia_class.get(bia_doc_id)
|
189
|
+
doc_db.delete_doc(attach_doc)
|
190
|
+
#node.__unset_userdata_key(:attachment_doc_id)
|
191
|
+
#node.__save
|
192
|
+
else
|
193
|
+
puts "Warning: Attempted to delete attachments when none existed"
|
194
|
+
end
|
195
|
+
node
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,536 @@
|
|
1
|
+
#Tinkit directory structure defined in lib/helpers/require_helpers
|
2
|
+
require Tinkit.midas 'bufs_data_structure'
|
3
|
+
require Tinkit.glue '/couchrest/couchrest_files_mgr'
|
4
|
+
require Tinkit.helpers 'log_helper'
|
5
|
+
require Tinkit.helpers 'hash_helpers'
|
6
|
+
|
7
|
+
module CouchRestViews
|
8
|
+
#FIXME MAJOR BUG
|
9
|
+
#Views should be an instance to a user class
|
10
|
+
#not a module, otherwise, last one set gets all the goodies
|
11
|
+
|
12
|
+
#Set Logger
|
13
|
+
@@log = TinkitLog.set(self.name, :warn)
|
14
|
+
|
15
|
+
#Constants (pulling out magic text embedded in program)
|
16
|
+
#Changing these will break compatibility with earlier records
|
17
|
+
ClassViewAllName = "all" #view name stored in the couch db design doc
|
18
|
+
ClassNamespaceKey = "all_this_class" #couch db record that the bufs node class name is stored
|
19
|
+
|
20
|
+
|
21
|
+
def self.set_view(db, design_doc, view_name, opts={})
|
22
|
+
#raise view_name if view_name == :parent_categories
|
23
|
+
#TODO: Add options for custom maps, etc
|
24
|
+
#creating view in design_doc
|
25
|
+
#puts "setting design_doc #{design_doc['_id']} with view: #{view_name.inspect} with map:\n #{opts.inspect}"
|
26
|
+
design_doc.view_by view_name.to_sym, opts
|
27
|
+
db_view_name = "by_#{view_name}"
|
28
|
+
views = design_doc['views'] || {}
|
29
|
+
view_keys = views.keys || []
|
30
|
+
unless view_keys.include? db_view_name
|
31
|
+
design_doc['_rev'] = nil
|
32
|
+
end
|
33
|
+
begin
|
34
|
+
view_rev_in_db = db.get(design_doc['_id'])['_rev']
|
35
|
+
#TODO: See if this can be simplified, I had forgotten the underscore for rev and added a bunch of other stuff
|
36
|
+
#I also think I'm saving when it's not needed because I can't figure out how to detect if the saved view matches the
|
37
|
+
#current view I want to run yet
|
38
|
+
design_doc_uptodate = (design_doc['_rev'] == view_rev_in_db) &&
|
39
|
+
(design_doc['views'].keys.include? db_view_name)
|
40
|
+
design_doc['_rev'] = view_rev_in_db #unless design_doc_uptodate
|
41
|
+
res = design_doc.save #unless design_doc_uptodate
|
42
|
+
@@log.debug { "Save Design Doc Response: #{res.inspect}"} if @@log.debug?
|
43
|
+
res
|
44
|
+
rescue RestClient::RequestFailed
|
45
|
+
if @@log.warn?
|
46
|
+
@@log.warn { "Warning: Request Failed, assuming because the design doc was already saved?"}
|
47
|
+
end
|
48
|
+
if @@log.info?
|
49
|
+
@@log.info { "Design doc_id: #{design_doc['_id'].inspect}"}
|
50
|
+
@@log.info { "doc_rev: #{design_doc['_rev'].inspect}" }
|
51
|
+
@@log.info { "db_rev: #{view_rev_in_db}" }
|
52
|
+
@@log.info {"Code thinks doc is up to date? #{design_doc_uptodate.inspect}" }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def self.set_view_all(db, design_doc, model_name, datastore_location)
|
59
|
+
view_name = "#{ClassViewAllName}_#{model_name}"
|
60
|
+
namespace_id = ClassNamespaceKey
|
61
|
+
record_namespace = "#{datastore_location}_#{model_name}"
|
62
|
+
map_str = "function(doc) {
|
63
|
+
if (doc['#{namespace_id}'] == '#{record_namespace}') {
|
64
|
+
emit(doc['_id'], doc);
|
65
|
+
}
|
66
|
+
}"
|
67
|
+
map_fn = { :map => map_str }
|
68
|
+
self.set_view(db, design_doc, view_name, map_fn)
|
69
|
+
end
|
70
|
+
|
71
|
+
#Set static views.
|
72
|
+
#=begin
|
73
|
+
def self.set_my_cat_view(db, design_doc, user_datastore_location)
|
74
|
+
map_str = "function(doc) {
|
75
|
+
if (doc.#{ClassNamespaceKey} =='#{user_datastore_location}' && doc.my_category ){
|
76
|
+
emit(doc.my_category, doc);
|
77
|
+
}
|
78
|
+
}"
|
79
|
+
map_fn = { :map => map_str }
|
80
|
+
#TODO: Tied to datastructure
|
81
|
+
self.set_view(db, design_doc, :my_category, map_fn)
|
82
|
+
end
|
83
|
+
#=end
|
84
|
+
#TODO: Tied to datastructure
|
85
|
+
def self.by_my_category(moab_data, user_datastore_location, match_key)
|
86
|
+
db = moab_data[:db]
|
87
|
+
design_doc = moab_data[:design_doc]
|
88
|
+
map_str = "function(doc) {
|
89
|
+
if (doc.bufs_namespace =='#{user_datastore_location}' && doc.my_category ){
|
90
|
+
emit(doc.my_category, doc);
|
91
|
+
}
|
92
|
+
}"
|
93
|
+
map_fn = { :map => map_str }
|
94
|
+
self.set_view(db, design_doc, :my_category, map_fn)
|
95
|
+
raw_res = design_doc.view :by_my_category, :key => match_key
|
96
|
+
rows = raw_res["rows"]
|
97
|
+
records = rows.map{|r| r["value"]}
|
98
|
+
end
|
99
|
+
|
100
|
+
#TODO: Tied to datastructure
|
101
|
+
def self.by_parent_categories(moab_data, user_datastore_location, match_keys)
|
102
|
+
db = moab_data[:db]
|
103
|
+
design_doc = moab_data[:design_doc]
|
104
|
+
map_str = "function(doc) {
|
105
|
+
if (doc.bufs_namespace == '#{user_datastore_location}' && doc.parent_categories) {
|
106
|
+
emit(doc.parent_categories, doc);
|
107
|
+
};
|
108
|
+
};"
|
109
|
+
# }"
|
110
|
+
map_fn = { :map => map_str }
|
111
|
+
|
112
|
+
self.set_view(db, design_doc, :parent_categories, map_fn)
|
113
|
+
raw_res = design_doc.view :by_parent_categories
|
114
|
+
rows = raw_res["rows"]
|
115
|
+
records = rows.map{|r| r["value"] if r["value"]["parent_categories"].include? match_keys}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
module CouchrestViews
|
119
|
+
DefineViews = {
|
120
|
+
:value_match => nil,
|
121
|
+
:included_match => nil,
|
122
|
+
:key_of_included_match => nil
|
123
|
+
}
|
124
|
+
|
125
|
+
def view_map(namespace_label, datastore_location, field_name)
|
126
|
+
"function(doc) {
|
127
|
+
if (doc.#{namespace_label} =='#{user_datastore_location}' && doc.#{field_name} ){
|
128
|
+
emit(doc.#{field_name}, doc);
|
129
|
+
}
|
130
|
+
}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_view_value_match(db, design_doc, namespace_key, user_datastore_location, field_name)
|
134
|
+
map_function = { :map => view_map(namespace_key, user_datastore_location, field_name) }
|
135
|
+
CouchRestViews.set_view(db, design_doc, field_name, map_function)
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
def call_view(field_name, moab_data, namespace_key, user_datastore_location, match_key, view_name = nil)
|
140
|
+
db = moab_data[:db]
|
141
|
+
design_doc = moab_data[:design_doc]
|
142
|
+
set_view_value_match(db, design_doc, namespace_key, user_datastore_location, field_name)
|
143
|
+
view_name = view_name || "by_#{field_name}"
|
144
|
+
raw_results = design_doc.view view_name, :key => match_key
|
145
|
+
rows = raw_results["rows"]
|
146
|
+
records = rows.map{|r| r["value"]}
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
module CouchrestEnv
|
152
|
+
#EnvName = :couchrest_env #name for couchrest environments
|
153
|
+
|
154
|
+
class GlueEnv
|
155
|
+
#Set Logger
|
156
|
+
@@log = TinkitLog.set(self.name, :warn)
|
157
|
+
|
158
|
+
include CouchRest::Mixins::Views::ClassMethods
|
159
|
+
include CouchrestViews
|
160
|
+
|
161
|
+
#used to identify metadata for models (should be consistent across models)
|
162
|
+
#ModelKey = :_id
|
163
|
+
VersionKey = :_rev
|
164
|
+
#NamespaceKey = :bufs_namespace
|
165
|
+
PersistLayerKey = :_id #required by CouchDb to be unique in DB
|
166
|
+
|
167
|
+
CouchMetadataKeys = [:_pos, :_deleted_conflicts, :couchrest, :all_this_class] #possibly more, these keys are ignored
|
168
|
+
QueryAllStr = "by_all_bufs".to_sym
|
169
|
+
AttachClassBaseName = "MoabAttachmentHandler"
|
170
|
+
DesignDocBaseName = "CouchRestEnv" #used to be module name
|
171
|
+
|
172
|
+
|
173
|
+
attr_accessor :user_id,
|
174
|
+
:user_datastore_location,
|
175
|
+
:metadata_keys,
|
176
|
+
:required_instance_keys,
|
177
|
+
:required_save_keys,
|
178
|
+
:node_key,
|
179
|
+
:model_key,
|
180
|
+
:version_key,
|
181
|
+
:namespace_key,
|
182
|
+
:_files_mgr_class,
|
183
|
+
:views,
|
184
|
+
:model_save_params,
|
185
|
+
:moab_data,
|
186
|
+
:persist_layer_key,
|
187
|
+
#accessors specific to this persitence model
|
188
|
+
:db,
|
189
|
+
:design_doc,
|
190
|
+
:query_all,
|
191
|
+
:attachment_base_id,
|
192
|
+
:attachClass
|
193
|
+
|
194
|
+
def initialize(persist_env, data_model_bindings)
|
195
|
+
couchrest_env = persist_env[:env]
|
196
|
+
couch_db_host = couchrest_env[:host]
|
197
|
+
db_name_path = couchrest_env[:path]
|
198
|
+
@user_id = couchrest_env[:user_id]
|
199
|
+
@model_name = persist_env[:name]
|
200
|
+
|
201
|
+
#data_model_bindings from NodeElementOperations
|
202
|
+
key_fields = data_model_bindings[:key_fields]
|
203
|
+
initial_views_data = data_model_bindings[:views] || []
|
204
|
+
|
205
|
+
#FIXME: Major BUG!! when setting multiple environments in that this may cross-contaminate across users
|
206
|
+
#if those users share the same db. Testing up to date has been users on different dbs, so not an issue to date
|
207
|
+
#also, one solution might be to force users to their own db? (what about sharing though?)
|
208
|
+
#The problem is that there is one "query_all" per database, and it gets set to the last user class
|
209
|
+
#that sets it. [Is this still a bug? 12/16/10]
|
210
|
+
#@user_id = db_user_id
|
211
|
+
#user_attach_class_name = "UserAttach#{db_user_id}"
|
212
|
+
#the rescue is so that testing works
|
213
|
+
#begin
|
214
|
+
# attachClass = UserNode.const_get(user_attach_class_name)
|
215
|
+
#rescue NameError
|
216
|
+
# puts "Warning:: Multiuser support for attachments not enabled. Using generic Attachment Class"
|
217
|
+
# attachClass = CouchrestAttachment
|
218
|
+
#end
|
219
|
+
couch_db_location = set_db_location(couch_db_host, db_name_path)
|
220
|
+
@db = CouchRest.database!(couch_db_location)
|
221
|
+
@model_save_params = {:db => @db}
|
222
|
+
@persist_layer_key = PersistLayerKey
|
223
|
+
|
224
|
+
#@collection_namespace = CouchrestEnv.set_collection_namespace(db_name_path, @user_id)
|
225
|
+
#@user_datastore_location = CouchRestEnv.set_user_datastore_location(@db, @user_id)
|
226
|
+
@user_datastore_location = set_namespace(db_name_path, @user_id)
|
227
|
+
@design_doc = set_couch_design(@db, @user_id)#, @collection_namespace)
|
228
|
+
@node_key = key_fields[:primary_key]
|
229
|
+
#
|
230
|
+
@define_query_all = QueryAllStr #CouchRestEnv.query_for_all_collection_records
|
231
|
+
|
232
|
+
@required_instance_keys = key_fields[:required_keys] #DataStructureModels::RequiredInstanceKeys
|
233
|
+
@required_save_keys = key_fields[:required_keys] #DataStructureModels::Tinkit::RequiredSaveKeys
|
234
|
+
@model_key = @node_key #ModelKey #CouchRestEnv::ModelKey
|
235
|
+
@version_key = VersionKey #CouchRestEnv::VersionKey
|
236
|
+
@namespace_key = @model_name #NamespaceKey #CouchRestEnv::NamespaceKey
|
237
|
+
#TODO: Need to investigate whether to keep model_key = node_key in metadata
|
238
|
+
@metadata_keys = [@persist_layer_key, @version_key, @namespace_key] + CouchMetadataKeys #CouchRestEnv.set_db_metadata_keys #(@collection_namespace)
|
239
|
+
|
240
|
+
@views = CouchRestViews
|
241
|
+
@views.set_view_all(@db, @design_doc, @model_name, @user_datastore_location)
|
242
|
+
|
243
|
+
@views.set_my_cat_view(@db, @design_doc, @user_datastore_location)
|
244
|
+
|
245
|
+
|
246
|
+
|
247
|
+
#set new view
|
248
|
+
initial_views_data.each do |view_name, view_data|
|
249
|
+
set_view_value_match(@db, @design_doc, @namespace_key, @user_datastore_location, view_data[:field])
|
250
|
+
end
|
251
|
+
|
252
|
+
#@views.set_new_views(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
|
253
|
+
|
254
|
+
attach_class_name = "#{AttachClassBaseName}#{@user_id}"
|
255
|
+
@attachClass = set_attach_class(@db.root, attach_class_name)
|
256
|
+
@moab_data = {:db => @db,
|
257
|
+
:design_doc => @design_doc,
|
258
|
+
:attachClass => @attachClass}
|
259
|
+
#TODO: Have to do the above, but want to do the below
|
260
|
+
#@attachClass = set_attach_class(@db.root, attach_class_name)
|
261
|
+
@_files_mgr_class = CouchrestInterface::FilesMgr
|
262
|
+
end
|
263
|
+
|
264
|
+
#TODO Need to fix some naming issues before bringing this method over into the glue environment
|
265
|
+
#def set_attach_class(db_root_location, attach_class_name)
|
266
|
+
# dyn_attach_class_def = "class #{attach_class_name} < CouchrestAttachment
|
267
|
+
# use_database CouchRest.database!(\"http://#{db_root_location}/\")
|
268
|
+
#
|
269
|
+
# def self.namespace
|
270
|
+
# CouchRest.database!(\"http://#{db_root_location}/\")
|
271
|
+
# end
|
272
|
+
# end"
|
273
|
+
#
|
274
|
+
# self.class.class_eval(dyn_attach_class_def)
|
275
|
+
# self.class.const_get(attach_class_name)
|
276
|
+
#end
|
277
|
+
|
278
|
+
def set_db_location(couch_db_host, db_name_path)
|
279
|
+
couch_db_host.chop if couch_db_host =~ /\/$/ #removes any trailing slash
|
280
|
+
db_name_path = "/#{db_name_path}" unless db_name_path =~ /^\// #check for le
|
281
|
+
couch_db_location = "#{couch_db_host}#{db_name_path}"
|
282
|
+
end
|
283
|
+
|
284
|
+
#TODO: MAJOR Refactoring may have broken compatibility with already persisted data, need to
|
285
|
+
#figure out tool to migrate persisted data when changes occur
|
286
|
+
#TODO: MAJOR Namespace should not be bound to the underlying model it should be bound to user data only
|
287
|
+
def set_namespace(db_name_path, db_user_id)
|
288
|
+
lose_leading_slash = db_name_path.split("/")
|
289
|
+
lose_leading_slash.shift
|
290
|
+
db_name = lose_leading_slash.join("")
|
291
|
+
#namespace = "#{db_name}_#{db_user_id}"
|
292
|
+
namespace = "#{db_user_id}"
|
293
|
+
end
|
294
|
+
|
295
|
+
def set_user_datastore_location(db, db_user_id)
|
296
|
+
"#{db.to_s}::#{db_user_id}"
|
297
|
+
end
|
298
|
+
|
299
|
+
def set_couch_design(db, user_id) #, view_name)
|
300
|
+
design_doc = CouchRest::Design.new
|
301
|
+
design_doc.name = "#{DesignDocBaseName}_#{user_id}_Design"
|
302
|
+
#example of a map function that can be passed as a parameter if desired (currently not needed)
|
303
|
+
#map_function = "function(doc) {\n if(doc['#{@@collection_namespace}']) {\n emit(doc['_id'], 1);\n }\n}"
|
304
|
+
#design_doc.view_by collection_namespace.to_sym #, {:map => map_function }
|
305
|
+
design_doc.database = db
|
306
|
+
begin
|
307
|
+
design_doc = db.get(design_doc['_id'])
|
308
|
+
rescue RestClient::ResourceNotFound
|
309
|
+
design_doc.save
|
310
|
+
end
|
311
|
+
design_doc
|
312
|
+
end
|
313
|
+
|
314
|
+
def set_attach_class(db_root_location, attach_class_name)
|
315
|
+
dyn_attach_class_def = "class #{attach_class_name} < CouchrestAttachment
|
316
|
+
use_database CouchRest.database!(\"http://#{db_root_location}/\")
|
317
|
+
|
318
|
+
def self.namespace
|
319
|
+
CouchRest.database!(\"http://#{db_root_location}/\")
|
320
|
+
end
|
321
|
+
end"
|
322
|
+
|
323
|
+
self.class.class_eval(dyn_attach_class_def)
|
324
|
+
self.class.const_get(attach_class_name)
|
325
|
+
end
|
326
|
+
|
327
|
+
def query_all #TODO move to ViewsMgr and change the confusing accessor/method clash
|
328
|
+
#breaks everything -> self.set_view(@db, @design_doc, @collection_namespace)
|
329
|
+
view_name = "by_#{CouchRestViews::ClassViewAllName}_#{@model_name}"
|
330
|
+
#raise view_name
|
331
|
+
raw_res = @design_doc.view view_name #@define_query_all
|
332
|
+
raw_data = raw_res["rows"]
|
333
|
+
data = raw_data.map {|rec| HashKeys.str_to_sym( rec['value'] ) }
|
334
|
+
end
|
335
|
+
|
336
|
+
#current relations supported:
|
337
|
+
# - :equals (data in the key field matches this_value)
|
338
|
+
# - :contains (this_value is contained in the key field data (same as equals for non-enumerable types )
|
339
|
+
def find_nodes_where(key, relation, this_value)
|
340
|
+
|
341
|
+
res = case relation
|
342
|
+
when :equals
|
343
|
+
find_equals(key, this_value)
|
344
|
+
when :contains
|
345
|
+
find_contains(key, this_value)
|
346
|
+
else
|
347
|
+
raise "Couldn't determine relationship between stored data and lookup data"
|
348
|
+
end #case
|
349
|
+
@@log.info {"Found #{res.size} nodes where #{key} #{relation} #{this_value}"} if @@log.info?
|
350
|
+
return res
|
351
|
+
end
|
352
|
+
|
353
|
+
def find_equals(key, this_value)
|
354
|
+
results =[]
|
355
|
+
query_all.each do |record|
|
356
|
+
test_val = record[key]
|
357
|
+
results << record if test_val == this_value
|
358
|
+
end
|
359
|
+
@@log.debug {"Found equals results: #{results.inspect}"} if @@log.debug?
|
360
|
+
results
|
361
|
+
end
|
362
|
+
|
363
|
+
def find_contains(key, this_value)
|
364
|
+
#TODO: Make a view for this rather than doing it in ruby
|
365
|
+
results =[]
|
366
|
+
query_all.each do |record|
|
367
|
+
test_val = record[key]
|
368
|
+
results << record if find_contains_type_helper(test_val, this_value)
|
369
|
+
end
|
370
|
+
@@log.debug {"Found contains results: #{results.inspect}"} if @@log.debug?
|
371
|
+
results
|
372
|
+
end
|
373
|
+
|
374
|
+
def find_contains_type_helper(stored_data, this_value)
|
375
|
+
resp = nil
|
376
|
+
#stored_data = jparse(stored_dataj)
|
377
|
+
if stored_data.respond_to?(:"include?")
|
378
|
+
resp = (stored_data.include?(this_value))
|
379
|
+
else
|
380
|
+
resp = (stored_data == this_value)
|
381
|
+
end
|
382
|
+
return resp
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
def save(user_data)
|
387
|
+
#I thinkthis was why I originally created the namespace concept but reconciliation is now required
|
388
|
+
pk_data = generate_pk_data(user_data[@model_key])
|
389
|
+
record_namespace = "#{@user_datastore_location}_#{@model_name}"
|
390
|
+
#Major TODO: Deconflict module CouchrestView and CouchRestViews
|
391
|
+
namespace_key = CouchRestViews::ClassNamespaceKey
|
392
|
+
pl_metadata = {PersistLayerKey => pk_data, namespace_key => record_namespace}
|
393
|
+
new_data = user_data.dup.merge(pl_metadata)
|
394
|
+
db = @model_save_params[:db]
|
395
|
+
|
396
|
+
raise "No database found to save data" unless db
|
397
|
+
raise "No CouchDB Key (#{PersistLayerKey}) found in data: #{new_data.inspect}" unless new_data[PersistLayerKey]
|
398
|
+
raise "No [#{@model_key}] key found in model data: #{new_data.inspect}" unless new_data[@model_key]
|
399
|
+
|
400
|
+
model_data = HashKeys.sym_to_str(new_data)
|
401
|
+
#db.save_doc(model_data)
|
402
|
+
begin
|
403
|
+
#TODO: Genericize this
|
404
|
+
res = db.save_doc(model_data)
|
405
|
+
rescue RestClient::RequestFailed => e
|
406
|
+
#TODO Update specs to test for this
|
407
|
+
if e.http_code == 409
|
408
|
+
doc_str = "Document Conflict in the Database."
|
409
|
+
@@log.warn { doc_str } if @@log.warn?
|
410
|
+
existing_doc = db.get(model_data['_id'])
|
411
|
+
rev = existing_doc['_rev']
|
412
|
+
data_with_rev = model_data.merge({'_rev' => rev})
|
413
|
+
res = db.save_doc(data_with_rev)
|
414
|
+
else
|
415
|
+
raise "Request Failed -- Response: #{res.inspect} Error:#{e}"\
|
416
|
+
"\nAdditonal Data: model params: #{model_save_params.inspect}"\
|
417
|
+
"\n model data: #{model_data.inspect}"\
|
418
|
+
"\n all data: #{new_data.inspect}"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def get(id)
|
424
|
+
#id can be the model id or the persist layer id
|
425
|
+
pk_data = if id.include? self.class.name
|
426
|
+
id
|
427
|
+
else
|
428
|
+
generate_pk_data(id)
|
429
|
+
end
|
430
|
+
#maybe put in some validations to ensure its from the proper collection namespace?
|
431
|
+
#pk_data = id #generate_pk_data(id)
|
432
|
+
#Major TODO: Deconflict module CouchrestView and CouchRestViews
|
433
|
+
namespace_key = CouchRestViews::ClassNamespaceKey
|
434
|
+
#options, use native couchdb _id or buidl a view for the model key
|
435
|
+
#currently opting for using _id, but this requires rebuilding the primary key data
|
436
|
+
#which is not an ideal solution
|
437
|
+
rtn = begin
|
438
|
+
node = @db.get(pk_data)
|
439
|
+
node = HashKeys.str_to_sym(node)
|
440
|
+
node.delete(PersistLayerKey)
|
441
|
+
node.delete(namespace_key)
|
442
|
+
node
|
443
|
+
rescue RestClient::ResourceNotFound => e
|
444
|
+
nil
|
445
|
+
end
|
446
|
+
rtn
|
447
|
+
end
|
448
|
+
|
449
|
+
#Not tested in factory tests (but is in couchrest tests)
|
450
|
+
def destroy_node(model_metadata)
|
451
|
+
#att_doc = node.my_GlueEnv.attachClass.get(node.attachment_doc_id) if node.respond_to?(:attachment_doc_id)
|
452
|
+
attachClass = @moab_data[:attachClass]
|
453
|
+
att_doc_id = attachClass.uniq_att_doc_id(model_metadata[@persist_layer_key])
|
454
|
+
att_doc = attachClass.get(att_doc_id) if att_doc_id
|
455
|
+
#raise "Destroying Attachment #{att_doc.inspect} derived from #{@model_metadata.inspect} "
|
456
|
+
att_doc.destroy if att_doc
|
457
|
+
db_destroy(model_metadata)
|
458
|
+
end
|
459
|
+
|
460
|
+
#TODO: update glue models so that it's id and rev that are explicitly needed
|
461
|
+
#which begs the question whether rev is supported or not
|
462
|
+
def db_destroy(model_metadata)
|
463
|
+
id_to_delete = model_metadata[@persist_layer_key] || generate_pk_data(model_metadata[@node_key])
|
464
|
+
@@log.info { "Destroy in DB, Key: #{id_to_delete.inspect} in #{model_metadata.inspect}" } if @@log.info?
|
465
|
+
|
466
|
+
rev_to_delete = model_metadata[@version_key] || @db.get(id_to_delete)['_rev']
|
467
|
+
doc_to_delete = {'_id' => id_to_delete, '_rev' => rev_to_delete }
|
468
|
+
|
469
|
+
@@log.debug { "Attempting to Delete: #{doc_to_delete.inspect}" } if @@log.debug?
|
470
|
+
begin
|
471
|
+
@db.delete_doc(doc_to_delete)
|
472
|
+
rescue ArgumentError => e
|
473
|
+
puts "Rescued Error: #{e} while trying to destroy #{model_metadata[@model_key]} node"
|
474
|
+
#Code here was deleting the latest version, but rather than wait for an error, it was proactively checked
|
475
|
+
#so this block may not be needed any more
|
476
|
+
#node = node.class.get(model_metadata[@model_key]) #(model_metadata['_id'])
|
477
|
+
#db_destroy(model_metadata)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
|
482
|
+
#def xdestroy_node(node)
|
483
|
+
# CouchRestEnv::destroy_node(node)
|
484
|
+
# node = nil
|
485
|
+
#end
|
486
|
+
|
487
|
+
#I hope this can be replaced by the generate_pk_data, but need to make sure
|
488
|
+
#FIXME: Actually it has to be
|
489
|
+
def generate_model_key(namespace, node_key_value)
|
490
|
+
#TODO: Make sure namespace is portable across model migrations (must be diff database)
|
491
|
+
#"#{namespace}::#{node_key}" # <== original if the below ends up breaking stuff
|
492
|
+
#"#{node_key}"
|
493
|
+
generate_pk_data(node_key_value)
|
494
|
+
end
|
495
|
+
|
496
|
+
def generate_pk_data(record_id)
|
497
|
+
#url_friendly_class_name = self.class.name.gsub('::','-')
|
498
|
+
"#{user_id}:#{self.class.name}:#{record_id}"
|
499
|
+
end
|
500
|
+
|
501
|
+
#some models have additional processing required, but not this one
|
502
|
+
def raw_all
|
503
|
+
query_all
|
504
|
+
end
|
505
|
+
|
506
|
+
|
507
|
+
|
508
|
+
#TODO: Investigate if Couchrest bulk actions or design views will assist here
|
509
|
+
#fixed to delete orphaned attachments, but this negates much of the advantage of using this method in the first place
|
510
|
+
#or perhaps using a close to the metal design view based on the class name?? (this may be better)
|
511
|
+
def destroy_bulk(user_records_to_delete)
|
512
|
+
#TODO: Investigate why mutiple ids may be returned for the same record
|
513
|
+
#Answer Database Corruption
|
514
|
+
user_records_to_delete.uniq!
|
515
|
+
#puts "List of all records: #{user_records_to_delete.map{|r| r['_id']}.inspect}"
|
516
|
+
|
517
|
+
user_records_to_delete.each do |user_rec|
|
518
|
+
begin
|
519
|
+
db_key = user_rec[@persist_layer_key] || generate_pk_data(user_rec[@model_key])
|
520
|
+
att_doc_id = attachClass.uniq_att_doc_id(db_key)
|
521
|
+
r = @db.get(db_key )
|
522
|
+
@db.delete_doc(r)
|
523
|
+
begin
|
524
|
+
att_doc = @db.get(att_doc_id)
|
525
|
+
rescue
|
526
|
+
att_doc = nil
|
527
|
+
end
|
528
|
+
@db.delete_doc(att_doc) if att_doc
|
529
|
+
rescue RestClient::RequestFailed
|
530
|
+
@@log.warn{ "Warning:: Failed to delete document?" } if @@log.warn?
|
531
|
+
end
|
532
|
+
end
|
533
|
+
nil #TODO ok to return nil if all docs destroyed? also, not verifying
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|