persistence-rails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/README.md +36 -0
- data/Rakefile +2 -0
- data/lib/generators/persistence/install_generator.rb +22 -0
- data/lib/generators/persistence/templates/application.js +10 -0
- data/lib/persistence-rails.rb +1 -0
- data/lib/persistence/rails.rb +8 -0
- data/lib/persistence/rails/engine.rb +6 -0
- data/lib/persistence/rails/version.rb +5 -0
- data/persistence-rails.gemspec +17 -0
- data/vendor/assets/javascript/persistence.all.js +16 -0
- data/vendor/assets/javascript/persistence.core.js +2419 -0
- data/vendor/assets/javascript/persistence.jquery.js +103 -0
- data/vendor/assets/javascript/persistence.jquery.mobile.js +256 -0
- data/vendor/assets/javascript/persistence.js +5 -0
- data/vendor/assets/javascript/persistence.migrations.js +303 -0
- data/vendor/assets/javascript/persistence.pool.js +47 -0
- data/vendor/assets/javascript/persistence.search.js +293 -0
- data/vendor/assets/javascript/persistence.store.appengine.js +412 -0
- data/vendor/assets/javascript/persistence.store.config.js +29 -0
- data/vendor/assets/javascript/persistence.store.memory.js +239 -0
- data/vendor/assets/javascript/persistence.store.mysql.js +127 -0
- data/vendor/assets/javascript/persistence.store.sql.js +900 -0
- data/vendor/assets/javascript/persistence.store.sqlite.js +123 -0
- data/vendor/assets/javascript/persistence.store.sqlite3.js +124 -0
- data/vendor/assets/javascript/persistence.store.titanium.js +193 -0
- data/vendor/assets/javascript/persistence.store.websql.js +218 -0
- data/vendor/assets/javascript/persistence.sync.js +353 -0
- metadata +76 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
//= require persistence.core
|
2
|
+
|
3
|
+
var sys = require('sys');
|
4
|
+
var mysql = require('mysql');
|
5
|
+
|
6
|
+
function log(o) {
|
7
|
+
sys.print(sys.inspect(o) + "\n");
|
8
|
+
}
|
9
|
+
|
10
|
+
function ConnectionPool(getSession, initialPoolSize) {
|
11
|
+
this.newConnection = getSession;
|
12
|
+
this.pool = [];
|
13
|
+
for(var i = 0; i < initialPoolSize; i++) {
|
14
|
+
this.pool.push({available: true, session: getSession()});
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
ConnectionPool.prototype.obtain = function() {
|
19
|
+
var session = null;
|
20
|
+
for(var i = 0; i < this.pool.length; i++) {
|
21
|
+
if(this.pool[i].available) {
|
22
|
+
var pool = this.pool[i];
|
23
|
+
session = pool.session;
|
24
|
+
pool.available = false;
|
25
|
+
pool.claimed = new Date();
|
26
|
+
break;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
if(!session) {
|
30
|
+
session = getSession();
|
31
|
+
this.pool.push({available: false, session: session, claimed: new Date() });
|
32
|
+
}
|
33
|
+
};
|
34
|
+
|
35
|
+
ConnectionPool.prototype.release = function(session) {
|
36
|
+
for(var i = 0; i < this.pool.length; i++) {
|
37
|
+
if(this.pool[i].session === session) {
|
38
|
+
var pool = this.pool[i];
|
39
|
+
pool.available = true;
|
40
|
+
pool.claimed = null;
|
41
|
+
return;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
return false;
|
45
|
+
};
|
46
|
+
|
47
|
+
exports.ConnectionPool = ConnectionPool;
|
@@ -0,0 +1,293 @@
|
|
1
|
+
//= require persistence.core
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @license
|
5
|
+
* Copyright (c) 2010 Zef Hemel <zef@zef.me>
|
6
|
+
*
|
7
|
+
* Permission is hereby granted, free of charge, to any person
|
8
|
+
* obtaining a copy of this software and associated documentation
|
9
|
+
* files (the "Software"), to deal in the Software without
|
10
|
+
* restriction, including without limitation the rights to use,
|
11
|
+
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
* copies of the Software, and to permit persons to whom the
|
13
|
+
* Software is furnished to do so, subject to the following
|
14
|
+
* conditions:
|
15
|
+
*
|
16
|
+
* The above copyright notice and this permission notice shall be
|
17
|
+
* included in all copies or substantial portions of the Software.
|
18
|
+
*
|
19
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
20
|
+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
21
|
+
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
22
|
+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
23
|
+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
24
|
+
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
25
|
+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
26
|
+
* OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
*/
|
28
|
+
|
29
|
+
try {
|
30
|
+
if(!window) {
|
31
|
+
window = {};
|
32
|
+
}
|
33
|
+
} catch(e) {
|
34
|
+
window = {};
|
35
|
+
exports.console = console;
|
36
|
+
}
|
37
|
+
|
38
|
+
var persistence = (window && window.persistence) ? window.persistence : {};
|
39
|
+
|
40
|
+
persistence.search = {};
|
41
|
+
|
42
|
+
persistence.search.config = function(persistence, dialect) {
|
43
|
+
var filteredWords = {'and':true, 'the': true, 'are': true};
|
44
|
+
|
45
|
+
var argspec = persistence.argspec;
|
46
|
+
|
47
|
+
function normalizeWord(word, filterShortWords) {
|
48
|
+
if(!(word in filteredWords || (filterShortWords && word.length < 3))) {
|
49
|
+
word = word.replace(/ies$/, 'y');
|
50
|
+
word = word.length > 3 ? word.replace(/s$/, '') : word;
|
51
|
+
return word;
|
52
|
+
} else {
|
53
|
+
return false;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Does extremely basic tokenizing of text. Also includes some basic stemming.
|
59
|
+
*/
|
60
|
+
function searchTokenizer(text) {
|
61
|
+
var words = text.toLowerCase().split(/[^\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/);
|
62
|
+
var wordDict = {};
|
63
|
+
// Prefixing words with _ to also index Javascript keywords and special fiels like 'constructor'
|
64
|
+
for(var i = 0; i < words.length; i++) {
|
65
|
+
var normalizedWord = normalizeWord(words[i]);
|
66
|
+
if(normalizedWord) {
|
67
|
+
var word = '_' + normalizedWord;
|
68
|
+
// Some extremely basic stemming
|
69
|
+
if(word in wordDict) {
|
70
|
+
wordDict[word]++;
|
71
|
+
} else {
|
72
|
+
wordDict[word] = 1;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
return wordDict;
|
77
|
+
}
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Parses a search query and returns it as list SQL parts later to be OR'ed or AND'ed.
|
81
|
+
*/
|
82
|
+
function searchPhraseParser(query, indexTbl, prefixByDefault) {
|
83
|
+
query = query.toLowerCase().replace(/['"]/, '').replace(/(^\s+|\s+$)/g, '');
|
84
|
+
var words = query.split(/\s+/);
|
85
|
+
var sqlParts = [];
|
86
|
+
var restrictedToColumn = null;
|
87
|
+
for(var i = 0; i < words.length; i++) {
|
88
|
+
var word = normalizeWord(words[i]);
|
89
|
+
if(!word) {
|
90
|
+
continue;
|
91
|
+
}
|
92
|
+
if(word.search(/:$/) !== -1) {
|
93
|
+
restrictedToColumn = word.substring(0, word.length-1);
|
94
|
+
continue;
|
95
|
+
}
|
96
|
+
var sql = '(';
|
97
|
+
if(word.search(/\*/) !== -1) {
|
98
|
+
sql += "`" + indexTbl + "`.`word` LIKE '" + word.replace(/\*/g, '%') + "'";
|
99
|
+
} else if(prefixByDefault) {
|
100
|
+
sql += "`" + indexTbl + "`.`word` LIKE '" + word + "%'";
|
101
|
+
} else {
|
102
|
+
sql += "`" + indexTbl + "`.`word` = '" + word + "'";
|
103
|
+
}
|
104
|
+
if(restrictedToColumn) {
|
105
|
+
sql += ' AND `' + indexTbl + "`.`prop` = '" + restrictedToColumn + "'";
|
106
|
+
}
|
107
|
+
sql += ')';
|
108
|
+
sqlParts.push(sql);
|
109
|
+
}
|
110
|
+
return sqlParts.length === 0 ? ["1=1"] : sqlParts;
|
111
|
+
}
|
112
|
+
|
113
|
+
var queryCollSubscribers = {}; // entityName -> subscription obj
|
114
|
+
persistence.searchQueryCollSubscribers = queryCollSubscribers;
|
115
|
+
|
116
|
+
function SearchFilter(query, entityName) {
|
117
|
+
this.query = query;
|
118
|
+
this.entityName = entityName;
|
119
|
+
}
|
120
|
+
|
121
|
+
SearchFilter.prototype.match = function (o) {
|
122
|
+
var meta = persistence.getMeta(this.entityName);
|
123
|
+
var query = this.query.toLowerCase();
|
124
|
+
var text = '';
|
125
|
+
for(var p in o) {
|
126
|
+
if(meta.textIndex.hasOwnProperty(p)) {
|
127
|
+
if(o[p]) {
|
128
|
+
text += o[p];
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
text = text.toLowerCase();
|
133
|
+
return text && text.indexOf(query) !== -1;
|
134
|
+
}
|
135
|
+
|
136
|
+
SearchFilter.prototype.sql = function (o) {
|
137
|
+
return "1=1";
|
138
|
+
}
|
139
|
+
|
140
|
+
SearchFilter.prototype.subscribeGlobally = function(coll, entityName) {
|
141
|
+
var meta = persistence.getMeta(entityName);
|
142
|
+
for(var p in meta.textIndex) {
|
143
|
+
if(meta.textIndex.hasOwnProperty(p)) {
|
144
|
+
persistence.subscribeToGlobalPropertyListener(coll, entityName, p);
|
145
|
+
}
|
146
|
+
}
|
147
|
+
};
|
148
|
+
|
149
|
+
SearchFilter.prototype.unsubscribeGlobally = function(coll, entityName) {
|
150
|
+
var meta = persistence.getMeta(entityName);
|
151
|
+
for(var p in meta.textIndex) {
|
152
|
+
if(meta.textIndex.hasOwnProperty(p)) {
|
153
|
+
persistence.unsubscribeFromGlobalPropertyListener(coll, entityName, p);
|
154
|
+
}
|
155
|
+
}
|
156
|
+
};
|
157
|
+
|
158
|
+
SearchFilter.prototype.toUniqueString = function() {
|
159
|
+
return "SEARCH: " + this.query;
|
160
|
+
}
|
161
|
+
|
162
|
+
function SearchQueryCollection(session, entityName, query, prefixByDefault) {
|
163
|
+
this.init(session, entityName, SearchQueryCollection);
|
164
|
+
this.subscribers = queryCollSubscribers[entityName];
|
165
|
+
this._filter = new SearchFilter(query, entityName);
|
166
|
+
|
167
|
+
|
168
|
+
if(query) {
|
169
|
+
this._additionalJoinSqls.push(', `' + entityName + '_Index`');
|
170
|
+
this._additionalWhereSqls.push('`root`.id = `' + entityName + '_Index`.`entityId`');
|
171
|
+
this._additionalWhereSqls.push('(' + searchPhraseParser(query, entityName + '_Index', prefixByDefault).join(' OR ') + ')');
|
172
|
+
this._additionalGroupSqls.push(' GROUP BY (`' + entityName + '_Index`.`entityId`)');
|
173
|
+
this._additionalGroupSqls.push(' ORDER BY SUM(`' + entityName + '_Index`.`occurrences`) DESC');
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
SearchQueryCollection.prototype = new persistence.DbQueryCollection();
|
178
|
+
|
179
|
+
SearchQueryCollection.prototype.oldClone = SearchQueryCollection.prototype.clone;
|
180
|
+
|
181
|
+
|
182
|
+
SearchQueryCollection.prototype.clone = function() {
|
183
|
+
var clone = this.oldClone(false);
|
184
|
+
var entityName = this._entityName;
|
185
|
+
clone.subscribers = queryCollSubscribers[entityName];
|
186
|
+
return clone;
|
187
|
+
};
|
188
|
+
|
189
|
+
SearchQueryCollection.prototype.order = function() {
|
190
|
+
throw new Error("Imposing additional orderings is not support for search query collections.");
|
191
|
+
};
|
192
|
+
|
193
|
+
/*
|
194
|
+
SearchQueryCollection.prototype.filter = function (property, operator, value) {
|
195
|
+
var c = this.clone();
|
196
|
+
c._filter = new persistence.AndFilter(this._filter, new persistence.PropertyFilter(property, operator, value));
|
197
|
+
// Add global listener (TODO: memory leak waiting to happen!)
|
198
|
+
//session.subscribeToGlobalPropertyListener(c, this._entityName, property);
|
199
|
+
return c;
|
200
|
+
};
|
201
|
+
*/
|
202
|
+
|
203
|
+
persistence.entityDecoratorHooks.push(function(Entity) {
|
204
|
+
/**
|
205
|
+
* Declares a property to be full-text indexed.
|
206
|
+
*/
|
207
|
+
Entity.textIndex = function(prop) {
|
208
|
+
if(!Entity.meta.textIndex) {
|
209
|
+
Entity.meta.textIndex = {};
|
210
|
+
}
|
211
|
+
Entity.meta.textIndex[prop] = true;
|
212
|
+
// Subscribe
|
213
|
+
var entityName = Entity.meta.name;
|
214
|
+
if(!queryCollSubscribers[entityName]) {
|
215
|
+
queryCollSubscribers[entityName] = {};
|
216
|
+
}
|
217
|
+
};
|
218
|
+
|
219
|
+
/**
|
220
|
+
* Returns a query collection representing the result of a search
|
221
|
+
* @param query an object with the following fields:
|
222
|
+
*/
|
223
|
+
Entity.search = function(session, query, prefixByDefault) {
|
224
|
+
var args = argspec.getArgs(arguments, [
|
225
|
+
{ name: 'session', optional: true, check: function(obj) { return obj.schemaSync; }, defaultValue: persistence },
|
226
|
+
{ name: 'query', optional: false, check: argspec.hasType('string') },
|
227
|
+
{ name: 'prefixByDefault', optional: false }
|
228
|
+
]);
|
229
|
+
session = args.session;
|
230
|
+
query = args.query;
|
231
|
+
prefixByDefault = args.prefixByDefault;
|
232
|
+
|
233
|
+
return session.uniqueQueryCollection(new SearchQueryCollection(session, Entity.meta.name, query, prefixByDefault));
|
234
|
+
};
|
235
|
+
});
|
236
|
+
|
237
|
+
persistence.schemaSyncHooks.push(function(tx) {
|
238
|
+
var entityMeta = persistence.getEntityMeta();
|
239
|
+
var queries = [];
|
240
|
+
for(var entityName in entityMeta) {
|
241
|
+
var meta = entityMeta[entityName];
|
242
|
+
if(meta.textIndex) {
|
243
|
+
queries.push([dialect.createTable(entityName + '_Index', [['entityId', 'VARCHAR(32)'], ['prop', 'VARCHAR(30)'], ['word', 'VARCHAR(100)'], ['occurrences', 'INT']]), null]);
|
244
|
+
queries.push([dialect.createIndex(entityName + '_Index', ['prop', 'word']), null]);
|
245
|
+
queries.push([dialect.createIndex(entityName + '_Index', ['word']), null]);
|
246
|
+
persistence.generatedTables[entityName + '_Index'] = true;
|
247
|
+
}
|
248
|
+
}
|
249
|
+
queries.reverse();
|
250
|
+
persistence.executeQueriesSeq(tx, queries);
|
251
|
+
});
|
252
|
+
|
253
|
+
|
254
|
+
persistence.flushHooks.push(function(session, tx, callback) {
|
255
|
+
var queries = [];
|
256
|
+
for (var id in session.getTrackedObjects()) {
|
257
|
+
if (session.getTrackedObjects().hasOwnProperty(id)) {
|
258
|
+
var obj = session.getTrackedObjects()[id];
|
259
|
+
var meta = session.define(obj._type).meta;
|
260
|
+
var indexTbl = obj._type + '_Index';
|
261
|
+
if(meta.textIndex) {
|
262
|
+
for ( var p in obj._dirtyProperties) {
|
263
|
+
if (obj._dirtyProperties.hasOwnProperty(p) && p in meta.textIndex) {
|
264
|
+
queries.push(['DELETE FROM `' + indexTbl + '` WHERE `entityId` = ? AND `prop` = ?', [id, p]]);
|
265
|
+
var occurrences = searchTokenizer(obj._data[p]);
|
266
|
+
for(var word in occurrences) {
|
267
|
+
if(occurrences.hasOwnProperty(word)) {
|
268
|
+
queries.push(['INSERT INTO `' + indexTbl + '` VALUES (?, ?, ?, ?)', [obj.id, p, word.substring(1), occurrences[word]]]);
|
269
|
+
}
|
270
|
+
}
|
271
|
+
}
|
272
|
+
}
|
273
|
+
}
|
274
|
+
}
|
275
|
+
}
|
276
|
+
for (var id in persistence.getObjectsToRemove()) {
|
277
|
+
if (persistence.getObjectsToRemove().hasOwnProperty(id)) {
|
278
|
+
var obj = persistence.getObjectsToRemove()[id];
|
279
|
+
var meta = persistence.getEntityMeta()[obj._type];
|
280
|
+
if(meta.textIndex) {
|
281
|
+
queries.push(['DELETE FROM `' + obj._type + '_Index` WHERE `entityId` = ?', [id]]);
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
queries.reverse();
|
286
|
+
persistence.executeQueriesSeq(tx, queries, callback);
|
287
|
+
});
|
288
|
+
};
|
289
|
+
|
290
|
+
if(typeof exports === 'object') {
|
291
|
+
exports.config = persistence.search.config;
|
292
|
+
}
|
293
|
+
|
@@ -0,0 +1,412 @@
|
|
1
|
+
//= require persistence.core
|
2
|
+
|
3
|
+
var jdatastore = Packages.com.google.appengine.api.datastore,
|
4
|
+
JDatastoreServiceFactory = jdatastore.DatastoreServiceFactory,
|
5
|
+
JKeyFactory = jdatastore.KeyFactory,
|
6
|
+
JDatastoreService = jdatastore.DatastoreService,
|
7
|
+
JFilterOperator = jdatastore.Query.FilterOperator,
|
8
|
+
JSortDirection = jdatastore.Query.SortDirection,
|
9
|
+
JQuery = jdatastore.Query,
|
10
|
+
JInteger = java.lang.Integer;
|
11
|
+
|
12
|
+
exports.config = function(persistence) {
|
13
|
+
var argspec = persistence.argspec;
|
14
|
+
|
15
|
+
exports.getSession = function() {
|
16
|
+
var session = new persistence.Session();
|
17
|
+
session.dsService = JDatastoreServiceFactory.getDatastoreService();
|
18
|
+
session.transaction = function (fn) {
|
19
|
+
fn({executeSql: function() {}});
|
20
|
+
};
|
21
|
+
|
22
|
+
session.close = function() { };
|
23
|
+
return session;
|
24
|
+
};
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Converts a value from the data store to a value suitable for the entity
|
28
|
+
* (also does type conversions, if necessary)
|
29
|
+
*/
|
30
|
+
function dbValToEntityVal(val, type) {
|
31
|
+
if (val === null || val === undefined) {
|
32
|
+
return val;
|
33
|
+
}
|
34
|
+
switch (type) {
|
35
|
+
case 'DATE':
|
36
|
+
// SQL is in seconds and JS in miliseconds
|
37
|
+
if (val > 1000000000000) {
|
38
|
+
// usually in seconds, but sometimes it's milliseconds
|
39
|
+
return new Date(parseInt(val, 10));
|
40
|
+
} else {
|
41
|
+
return new Date(parseInt(val, 10) * 1000);
|
42
|
+
}
|
43
|
+
case 'BOOL':
|
44
|
+
return val === 1 || val === '1';
|
45
|
+
break;
|
46
|
+
case 'INT':
|
47
|
+
return +val;
|
48
|
+
break;
|
49
|
+
case 'BIGINT':
|
50
|
+
return +val;
|
51
|
+
break;
|
52
|
+
case 'JSON':
|
53
|
+
if (val) {
|
54
|
+
return JSON.parse(val);
|
55
|
+
}
|
56
|
+
else {
|
57
|
+
return val;
|
58
|
+
}
|
59
|
+
break;
|
60
|
+
default:
|
61
|
+
return val;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Converts an entity value to a data store value, inverse of
|
67
|
+
* dbValToEntityVal
|
68
|
+
*/
|
69
|
+
function entityValToDbVal(val, type) {
|
70
|
+
if (val === undefined || val === null) {
|
71
|
+
return null;
|
72
|
+
} else if (type === 'JSON' && val) {
|
73
|
+
return JSON.stringify(val);
|
74
|
+
} else if (val.id) {
|
75
|
+
return val.id;
|
76
|
+
} else if (type === 'BOOL') {
|
77
|
+
return (val === 'false') ? 0 : (val ? 1 : 0);
|
78
|
+
} else if (type === 'DATE' || val.getTime) {
|
79
|
+
val = new Date(val);
|
80
|
+
return new JInteger(Math.round(val.getTime() / 1000));
|
81
|
+
} else {
|
82
|
+
return val;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Converts a data store entity to an entity object
|
88
|
+
*/
|
89
|
+
function aeEntityToEntity(session, aeEnt, Entity) {
|
90
|
+
if(!aeEnt) return null;
|
91
|
+
|
92
|
+
var o = new Entity(session);
|
93
|
+
var meta = Entity.meta;
|
94
|
+
o.id = aeEnt.key.name;
|
95
|
+
var propMap = aeEnt.properties;
|
96
|
+
for(var prop in Iterator(propMap.keySet())) {
|
97
|
+
persistence.set(o, prop, dbValToEntityVal(propMap.get(prop), meta.fields[prop]));
|
98
|
+
}
|
99
|
+
return o;
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Converts a data store entity to an entity object
|
104
|
+
*/
|
105
|
+
function entityToAEEntity(meta, o) {
|
106
|
+
var ent = new jdatastore.Entity(o._type, o.id);
|
107
|
+
for(var k in meta.fields) {
|
108
|
+
if(meta.fields.hasOwnProperty(k)) {
|
109
|
+
ent.setProperty(k, entityValToDbVal(o._data[k], meta.fields[k]));
|
110
|
+
}
|
111
|
+
}
|
112
|
+
for(var k in meta.hasOne) {
|
113
|
+
if(meta.hasOne.hasOwnProperty(k)) {
|
114
|
+
ent.setProperty(k, entityValToDbVal(o._data[k], meta.fields[k]));
|
115
|
+
}
|
116
|
+
}
|
117
|
+
return ent;
|
118
|
+
}
|
119
|
+
|
120
|
+
var allEntities = [];
|
121
|
+
|
122
|
+
persistence.schemaSync = function (tx, callback, emulate) {
|
123
|
+
var args = argspec.getArgs(arguments, [
|
124
|
+
{ name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null },
|
125
|
+
{ name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} },
|
126
|
+
{ name: "emulate", optional: true, check: argspec.hasType('boolean') }
|
127
|
+
]);
|
128
|
+
tx = args.tx;
|
129
|
+
callback = args.callback;
|
130
|
+
|
131
|
+
var entityMeta = persistence.getEntityMeta();
|
132
|
+
for (var entityName in entityMeta) {
|
133
|
+
if (entityMeta.hasOwnProperty(entityName)) {
|
134
|
+
allEntities.push(persistence.define(entityName));
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
callback();
|
139
|
+
};
|
140
|
+
|
141
|
+
persistence.flush = function (tx, callback) {
|
142
|
+
var args = argspec.getArgs(arguments, [
|
143
|
+
{ name: "tx", optional: true, check: persistence.isTransaction },
|
144
|
+
{ name: "callback", optional: true, check: argspec.isCallback(), defaultValue: null }
|
145
|
+
]);
|
146
|
+
tx = args.tx;
|
147
|
+
callback = args.callback;
|
148
|
+
|
149
|
+
var session = this;
|
150
|
+
var fns = persistence.flushHooks;
|
151
|
+
persistence.asyncForEach(fns, function(fn, callback) {
|
152
|
+
fn(session, tx, callback);
|
153
|
+
}, function() {
|
154
|
+
// After applying the hooks
|
155
|
+
var entitiesToPersist = [];
|
156
|
+
var removeArrayList = new java.util.ArrayList();
|
157
|
+
|
158
|
+
for (var id in session.trackedObjects) {
|
159
|
+
if (session.trackedObjects.hasOwnProperty(id)) {
|
160
|
+
var ent = prepareDbEntity(session.trackedObjects[id]);
|
161
|
+
if(ent) {
|
162
|
+
entitiesToPersist.push(ent);
|
163
|
+
}
|
164
|
+
}
|
165
|
+
}
|
166
|
+
for (var id in session.objectsToRemove) {
|
167
|
+
if (session.objectsToRemove.hasOwnProperty(id)) {
|
168
|
+
removeArrayList.add(JKeyFactory.createKey(session.trackedObjects[id]._type, id));
|
169
|
+
delete session.trackedObjects[id]; // Stop tracking
|
170
|
+
}
|
171
|
+
}
|
172
|
+
if(entitiesToPersist.length > 0) {
|
173
|
+
session.dsService.put(entitiesToPersist);
|
174
|
+
}
|
175
|
+
if(removeArrayList.size() > 0) {
|
176
|
+
session.dsService['delete'](removeArrayList);
|
177
|
+
}
|
178
|
+
callback();
|
179
|
+
});
|
180
|
+
};
|
181
|
+
|
182
|
+
persistence.reset = function (tx, callback) {
|
183
|
+
var args = argspec.getArgs(arguments, [
|
184
|
+
{ name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null },
|
185
|
+
{ name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} }
|
186
|
+
]);
|
187
|
+
callback = args.callback;
|
188
|
+
|
189
|
+
var session = this;
|
190
|
+
|
191
|
+
persistence.asyncParForEach(allEntities, function(Entity, callback) {
|
192
|
+
Entity.all(session).destroyAll(callback);
|
193
|
+
}, callback);
|
194
|
+
};
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Internal function to persist an object to the database
|
198
|
+
* this function is invoked by persistence.flush()
|
199
|
+
*/
|
200
|
+
function prepareDbEntity(obj) {
|
201
|
+
var meta = persistence.getMeta(obj._type);
|
202
|
+
var isDirty = obj._new;
|
203
|
+
if(Object.keys(obj._dirtyProperties).length > 0) {
|
204
|
+
isDirty = true;
|
205
|
+
}
|
206
|
+
if(isDirty) {
|
207
|
+
return entityToAEEntity(meta, obj);
|
208
|
+
} else {
|
209
|
+
return null; // no saving required
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
/////////////////////////// QueryCollection patches to work in AppEngine environment
|
214
|
+
|
215
|
+
persistence.NullFilter.prototype.addFilter = function (meta, query) {
|
216
|
+
};
|
217
|
+
|
218
|
+
persistence.AndFilter.prototype.addFilter = function (meta, query) {
|
219
|
+
this.left.addFilter(meta, query);
|
220
|
+
this.right.addFilter(meta, query);
|
221
|
+
};
|
222
|
+
|
223
|
+
persistence.OrFilter.prototype.addFilter = function (meta, query) {
|
224
|
+
throw new Error("OrFilter Not supported");
|
225
|
+
};
|
226
|
+
|
227
|
+
persistence.PropertyFilter.prototype.addFilter = function (meta, query) {
|
228
|
+
var filterOp;
|
229
|
+
var value = this.value;
|
230
|
+
switch(this.operator) {
|
231
|
+
case '=':
|
232
|
+
filterOp = JFilterOperator.EQUAL;
|
233
|
+
break;
|
234
|
+
case '!=':
|
235
|
+
filterOp = JFilterOperator.NOT_EQUAL;
|
236
|
+
break;
|
237
|
+
case '>':
|
238
|
+
filterOp = JFilterOperator.GREATER_THAN;
|
239
|
+
break;
|
240
|
+
case '>=':
|
241
|
+
filterOp = JFilterOperator.GREATER_THAN_OR_EQUAL;
|
242
|
+
break;
|
243
|
+
case '<':
|
244
|
+
filterOp = JFilterOperator.LESS_THAN;
|
245
|
+
break;
|
246
|
+
case '<=':
|
247
|
+
filterOp = JFilterOperator.LESS_THAN_OR_EQUAL;
|
248
|
+
break;
|
249
|
+
case 'in':
|
250
|
+
var values = new java.util.ArrayList();
|
251
|
+
var type = meta.fields[this.property];
|
252
|
+
for(var i = 0; i < value.length; i++) {
|
253
|
+
values.add(entityValToDbVal(value[i], type));
|
254
|
+
}
|
255
|
+
value = values;
|
256
|
+
filterOp = JFilterOperator.IN;
|
257
|
+
break;
|
258
|
+
};
|
259
|
+
query.addFilter(this.property, filterOp, entityValToDbVal(value, meta.fields[this.property]));
|
260
|
+
};
|
261
|
+
|
262
|
+
|
263
|
+
function prepareQuery(coll, callback) {
|
264
|
+
var session = coll._session;
|
265
|
+
var entityName = coll._entityName;
|
266
|
+
var meta = persistence.getMeta(entityName);
|
267
|
+
|
268
|
+
// handles mixin case -- this logic is generic and could be in persistence.
|
269
|
+
if (meta.isMixin) {
|
270
|
+
var result = [];
|
271
|
+
persistence.asyncForEach(meta.mixedIns, function(realMeta, next) {
|
272
|
+
var query = coll.clone();
|
273
|
+
query._entityName = realMeta.name;
|
274
|
+
query.list(tx, function(array) {
|
275
|
+
result = result.concat(array);
|
276
|
+
next();
|
277
|
+
});
|
278
|
+
}, function() {
|
279
|
+
var query = new persistence.LocalQueryCollection(result);
|
280
|
+
query._orderColumns = coll._orderColumns;
|
281
|
+
query._reverse = coll._reverse;
|
282
|
+
// TODO: handle skip and limit -- do we really want to do it?
|
283
|
+
query.list(null, callback);
|
284
|
+
});
|
285
|
+
return;
|
286
|
+
}
|
287
|
+
|
288
|
+
var query = new JQuery(entityName); // Not tbe confused with jQuery
|
289
|
+
coll._filter.addFilter(meta, query);
|
290
|
+
|
291
|
+
coll._orderColumns.forEach(function(col) {
|
292
|
+
query.addSort(col[0], col[1] ? JSortDirection.ASCENDING : JSortDirection.DESCENDING);
|
293
|
+
});
|
294
|
+
|
295
|
+
callback(session.dsService.prepare(query));
|
296
|
+
}
|
297
|
+
|
298
|
+
|
299
|
+
/**
|
300
|
+
* Asynchronous call to actually fetch the items in the collection
|
301
|
+
* @param tx transaction to use
|
302
|
+
* @param callback function to be called taking an array with
|
303
|
+
* result objects as argument
|
304
|
+
*/
|
305
|
+
persistence.DbQueryCollection.prototype.list = function (tx, callback) {
|
306
|
+
var args = argspec.getArgs(arguments, [
|
307
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
308
|
+
{ name: 'callback', optional: false, check: argspec.isCallback() }
|
309
|
+
]);
|
310
|
+
callback = args.callback;
|
311
|
+
var that = this;
|
312
|
+
var entityName = this._entityName;
|
313
|
+
var session = this._session;
|
314
|
+
|
315
|
+
// TODO: Check if filtering for 'id' property, then use key lookup
|
316
|
+
//
|
317
|
+
if(this._filter.right && this._filter.right.property && this._filter.right.property === 'id') {
|
318
|
+
var idToLoad = this._filter.right.value;
|
319
|
+
var obj;
|
320
|
+
try {
|
321
|
+
obj = session.dsService.get(JKeyFactory.createKey(entityName, idToLoad));
|
322
|
+
} catch(e) { // com.google.appengine.api.datastore.EntityNotFoundException
|
323
|
+
obj = null;
|
324
|
+
}
|
325
|
+
if(obj) {
|
326
|
+
callback([aeEntityToEntity(session, obj, persistence.define(entityName))]);
|
327
|
+
} else {
|
328
|
+
callback([]);
|
329
|
+
}
|
330
|
+
} else {
|
331
|
+
session.flush(function() {
|
332
|
+
prepareQuery(that, function(preparedQuery) {
|
333
|
+
if(that._limit === 1) {
|
334
|
+
var row = preparedQuery.asSingleEntity();
|
335
|
+
var e = aeEntityToEntity(session, row, persistence.define(entityName));
|
336
|
+
callback([e]);
|
337
|
+
} else {
|
338
|
+
var rows = preparedQuery.asList(jdatastore.FetchOptions.Builder.withLimit(that._limit === -1 ? 1000 : that._limit).offset(that._skip));
|
339
|
+
var results = [];
|
340
|
+
|
341
|
+
var Entity = persistence.define(entityName);
|
342
|
+
for (var i = 0; i < rows.size(); i++) {
|
343
|
+
var r = rows.get(i);
|
344
|
+
var e = aeEntityToEntity(session, r, Entity);
|
345
|
+
results.push(e);
|
346
|
+
session.add(e);
|
347
|
+
}
|
348
|
+
if(that._reverse) {
|
349
|
+
results.reverse();
|
350
|
+
}
|
351
|
+
that.triggerEvent('list', that, results);
|
352
|
+
callback(results);
|
353
|
+
}
|
354
|
+
});
|
355
|
+
});
|
356
|
+
}
|
357
|
+
};
|
358
|
+
|
359
|
+
persistence.DbQueryCollection.prototype.destroyAll = function (tx, callback) {
|
360
|
+
var args = argspec.getArgs(arguments, [
|
361
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
362
|
+
{ name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} }
|
363
|
+
]);
|
364
|
+
callback = args.callback;
|
365
|
+
|
366
|
+
var that = this;
|
367
|
+
var session = this._session;
|
368
|
+
|
369
|
+
session.flush(function() {
|
370
|
+
prepareQuery(that, function(preparedQuery) {
|
371
|
+
var rows = preparedQuery.asList(jdatastore.FetchOptions.Builder.withLimit(that._limit === -1 ? 1000 : that._limit).offset(that._skip));
|
372
|
+
var keys = new java.util.ArrayList();
|
373
|
+
for (var i = 0; i < rows.size(); i++) {
|
374
|
+
var r = rows.get(i);
|
375
|
+
keys.add(r.getKey());
|
376
|
+
}
|
377
|
+
that._session.dsService['delete'](keys);
|
378
|
+
that.triggerEvent('change', that);
|
379
|
+
callback();
|
380
|
+
});
|
381
|
+
});
|
382
|
+
};
|
383
|
+
|
384
|
+
persistence.DbQueryCollection.prototype.count = function (tx, callback) {
|
385
|
+
var args = argspec.getArgs(arguments, [
|
386
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
387
|
+
{ name: 'callback', optional: false, check: argspec.isCallback() }
|
388
|
+
]);
|
389
|
+
tx = args.tx;
|
390
|
+
callback = args.callback;
|
391
|
+
|
392
|
+
var that = this;
|
393
|
+
var session = this._session;
|
394
|
+
|
395
|
+
session.flush(function() {
|
396
|
+
prepareQuery(that, function(preparedQuery) {
|
397
|
+
var n = preparedQuery.countEntities(jdatastore.FetchOptions.Builder.withDefaults());
|
398
|
+
callback(n);
|
399
|
+
});
|
400
|
+
});
|
401
|
+
|
402
|
+
};
|
403
|
+
|
404
|
+
persistence.isSession = function(obj) {
|
405
|
+
var isSession = !obj || (obj && obj.schemaSync);
|
406
|
+
if(!isSession) {
|
407
|
+
throw Error("No session argument passed, you should!");
|
408
|
+
}
|
409
|
+
return isSession;
|
410
|
+
};
|
411
|
+
};
|
412
|
+
|