persistence-rails 0.0.1
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/.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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# persistence-rails
|
2
|
+
|
3
|
+
## Rails integration for Persistence.js
|
4
|
+
|
5
|
+
The gem adds @zefhemel's [Persistence.js](http://persistencejs.org/) to your Rails project.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'persistence-rails'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install persistence-rails
|
20
|
+
|
21
|
+
You can also run the generator to add Persistence.js to your application.js automatically.
|
22
|
+
By default it only allows access to a selection of Persistence.js modules, if you want to load all of them use
|
23
|
+
|
24
|
+
//= require persistence.all
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
TODO: Write usage instructions here
|
29
|
+
|
30
|
+
## Contributing
|
31
|
+
|
32
|
+
1. Fork it
|
33
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
34
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
35
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
36
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Persistence
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
|
7
|
+
source_root File.expand_path("../templates", __FILE__)
|
8
|
+
desc "This generator installs Persistence.js to Asset Pipeline"
|
9
|
+
|
10
|
+
def add_assets
|
11
|
+
|
12
|
+
if File.exist?('app/assets/javascripts/application.js')
|
13
|
+
insert_into_file "app/assets/javascripts/application.js", "//= require persistence\n", :after => "jquery_ujs\n"
|
14
|
+
else
|
15
|
+
copy_file "application.js", "app/assets/javascripts/application.js"
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into including all the files listed below.
|
2
|
+
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
|
3
|
+
// be included in the compiled file accessible from http://example.com/assets/application.js
|
4
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
5
|
+
// the compiled file.
|
6
|
+
//
|
7
|
+
//= require jquery
|
8
|
+
//= require jquery_ujs
|
9
|
+
//= require persistence
|
10
|
+
//= require_tree .
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'persistence/rails'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/persistence/rails/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Alessandro Mencarini"]
|
6
|
+
gem.email = ["a.mencarini@freegoweb.it"]
|
7
|
+
gem.description = %q{Rails integration for Persistence.js}
|
8
|
+
gem.summary = %q{persistence-rails integrates client-side database ORM Persistence.js with Rails 3.1 asset pipeline}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "persistence-rails"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Persistence::Rails::VERSION
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
//= require persistence.core
|
2
|
+
//= require persistence.store.config
|
3
|
+
//= require persistence.store.memory
|
4
|
+
//= require persistence.store.mysql
|
5
|
+
//= require persistence.store.sql
|
6
|
+
//= require persistence.store.sqlite
|
7
|
+
//= require persistence.store.sqlite3
|
8
|
+
//= require persistence.store.titanium
|
9
|
+
//= require persistence.store.websql
|
10
|
+
//= require persistence.store.appengine
|
11
|
+
//= require persistence.sync
|
12
|
+
//= require persistence.jquery
|
13
|
+
//= require persistence.jquery.mobile
|
14
|
+
//= require persistence.migrations
|
15
|
+
//= require persistence.pool
|
16
|
+
//= require persistence.search
|
@@ -0,0 +1,2419 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2010 Zef Hemel <zef@zef.me>
|
3
|
+
*
|
4
|
+
* Permission is hereby granted, free of charge, to any person
|
5
|
+
* obtaining a copy of this software and associated documentation
|
6
|
+
* files (the "Software"), to deal in the Software without
|
7
|
+
* restriction, including without limitation the rights to use,
|
8
|
+
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
* copies of the Software, and to permit persons to whom the
|
10
|
+
* Software is furnished to do so, subject to the following
|
11
|
+
* conditions:
|
12
|
+
*
|
13
|
+
* The above copyright notice and this permission notice shall be
|
14
|
+
* included in all copies or substantial portions of the Software.
|
15
|
+
*
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
* OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
*/
|
25
|
+
|
26
|
+
if (typeof exports !== 'undefined') {
|
27
|
+
exports.createPersistence = function() {
|
28
|
+
return initPersistence({})
|
29
|
+
}
|
30
|
+
var singleton;
|
31
|
+
if (typeof (exports.__defineGetter__) === 'function') {
|
32
|
+
exports.__defineGetter__("persistence", function () {
|
33
|
+
if (!singleton)
|
34
|
+
singleton = exports.createPersistence();
|
35
|
+
return singleton;
|
36
|
+
});
|
37
|
+
} else {
|
38
|
+
Object.defineProperty(exports, "persistence", {
|
39
|
+
get: function () {
|
40
|
+
if (!singleton)
|
41
|
+
singleton = exports.createPersistence();
|
42
|
+
return singleton;
|
43
|
+
},
|
44
|
+
enumerable: true, configurable: true
|
45
|
+
});
|
46
|
+
}
|
47
|
+
|
48
|
+
}
|
49
|
+
else {
|
50
|
+
window = window || {};
|
51
|
+
window.persistence = initPersistence(window.persistence || {});
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
function initPersistence(persistence) {
|
56
|
+
if (persistence.isImmutable) // already initialized
|
57
|
+
return persistence;
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Check for immutable fields
|
61
|
+
*/
|
62
|
+
persistence.isImmutable = function(fieldName) {
|
63
|
+
return (fieldName == "id");
|
64
|
+
};
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Default implementation for entity-property
|
68
|
+
*/
|
69
|
+
persistence.defineProp = function(scope, field, setterCallback, getterCallback) {
|
70
|
+
if (typeof (scope.__defineSetter__) === 'function' && typeof (scope.__defineGetter__) === 'function') {
|
71
|
+
scope.__defineSetter__(field, function (value) {
|
72
|
+
setterCallback(value);
|
73
|
+
});
|
74
|
+
scope.__defineGetter__(field, function () {
|
75
|
+
return getterCallback();
|
76
|
+
});
|
77
|
+
} else {
|
78
|
+
Object.defineProperty(scope, field, {
|
79
|
+
get: getterCallback,
|
80
|
+
set: function (value) {
|
81
|
+
setterCallback(value);
|
82
|
+
},
|
83
|
+
enumerable: true, configurable: true
|
84
|
+
});
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Default implementation for entity-property setter
|
90
|
+
*/
|
91
|
+
persistence.set = function(scope, fieldName, value) {
|
92
|
+
if (persistence.isImmutable(fieldName)) throw new Error("immutable field: "+fieldName);
|
93
|
+
scope[fieldName] = value;
|
94
|
+
};
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Default implementation for entity-property getter
|
98
|
+
*/
|
99
|
+
persistence.get = function(arg1, arg2) {
|
100
|
+
return (arguments.length == 1) ? arg1 : arg1[arg2];
|
101
|
+
};
|
102
|
+
|
103
|
+
|
104
|
+
(function () {
|
105
|
+
var entityMeta = {};
|
106
|
+
var entityClassCache = {};
|
107
|
+
persistence.getEntityMeta = function() { return entityMeta; }
|
108
|
+
|
109
|
+
// Per-session data
|
110
|
+
persistence.trackedObjects = {};
|
111
|
+
persistence.objectsToRemove = {};
|
112
|
+
persistence.objectsRemoved = []; // {id: ..., type: ...}
|
113
|
+
persistence.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj
|
114
|
+
persistence.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection
|
115
|
+
|
116
|
+
persistence.getObjectsToRemove = function() { return this.objectsToRemove; };
|
117
|
+
persistence.getTrackedObjects = function() { return this.trackedObjects; };
|
118
|
+
|
119
|
+
// Public Extension hooks
|
120
|
+
persistence.entityDecoratorHooks = [];
|
121
|
+
persistence.flushHooks = [];
|
122
|
+
persistence.schemaSyncHooks = [];
|
123
|
+
|
124
|
+
// Enable debugging (display queries using console.log etc)
|
125
|
+
persistence.debug = true;
|
126
|
+
|
127
|
+
persistence.subscribeToGlobalPropertyListener = function(coll, entityName, property) {
|
128
|
+
var key = entityName + '__' + property;
|
129
|
+
if(key in this.globalPropertyListeners) {
|
130
|
+
var listeners = this.globalPropertyListeners[key];
|
131
|
+
for(var i = 0; i < listeners.length; i++) {
|
132
|
+
if(listeners[i] === coll) {
|
133
|
+
return;
|
134
|
+
}
|
135
|
+
}
|
136
|
+
this.globalPropertyListeners[key].push(coll);
|
137
|
+
} else {
|
138
|
+
this.globalPropertyListeners[key] = [coll];
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
persistence.unsubscribeFromGlobalPropertyListener = function(coll, entityName, property) {
|
143
|
+
var key = entityName + '__' + property;
|
144
|
+
var listeners = this.globalPropertyListeners[key];
|
145
|
+
for(var i = 0; i < listeners.length; i++) {
|
146
|
+
if(listeners[i] === coll) {
|
147
|
+
listeners.splice(i, 1);
|
148
|
+
return;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
persistence.propertyChanged = function(obj, property, oldValue, newValue) {
|
154
|
+
if(!this.trackedObjects[obj.id]) return; // not yet added, ignore for now
|
155
|
+
|
156
|
+
var entityName = obj._type;
|
157
|
+
var key = entityName + '__' + property;
|
158
|
+
if(key in this.globalPropertyListeners) {
|
159
|
+
var listeners = this.globalPropertyListeners[key];
|
160
|
+
for(var i = 0; i < listeners.length; i++) {
|
161
|
+
var coll = listeners[i];
|
162
|
+
var dummyObj = obj._data;
|
163
|
+
dummyObj[property] = oldValue;
|
164
|
+
var matchedBefore = coll._filter.match(dummyObj);
|
165
|
+
dummyObj[property] = newValue;
|
166
|
+
var matchedAfter = coll._filter.match(dummyObj);
|
167
|
+
if(matchedBefore != matchedAfter) {
|
168
|
+
coll.triggerEvent('change', coll, obj);
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
persistence.objectRemoved = function(obj) {
|
175
|
+
var entityName = obj._type;
|
176
|
+
if(this.queryCollectionCache[entityName]) {
|
177
|
+
var colls = this.queryCollectionCache[entityName];
|
178
|
+
for(var key in colls) {
|
179
|
+
if(colls.hasOwnProperty(key)) {
|
180
|
+
var coll = colls[key];
|
181
|
+
if(coll._filter.match(obj)) { // matched the filter -> was part of collection
|
182
|
+
coll.triggerEvent('change', coll, obj);
|
183
|
+
}
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
/**
|
190
|
+
* Retrieves metadata about entity, mostly for internal use
|
191
|
+
*/
|
192
|
+
function getMeta(entityName) {
|
193
|
+
return entityMeta[entityName];
|
194
|
+
}
|
195
|
+
|
196
|
+
persistence.getMeta = getMeta;
|
197
|
+
|
198
|
+
|
199
|
+
/**
|
200
|
+
* A database session
|
201
|
+
*/
|
202
|
+
function Session(conn) {
|
203
|
+
this.trackedObjects = {};
|
204
|
+
this.objectsToRemove = {};
|
205
|
+
this.objectsRemoved = [];
|
206
|
+
this.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj
|
207
|
+
this.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection
|
208
|
+
this.conn = conn;
|
209
|
+
}
|
210
|
+
|
211
|
+
Session.prototype = persistence; // Inherit everything from the root persistence object
|
212
|
+
|
213
|
+
persistence.Session = Session;
|
214
|
+
|
215
|
+
/**
|
216
|
+
* Define an entity
|
217
|
+
*
|
218
|
+
* @param entityName
|
219
|
+
* the name of the entity (also the table name in the database)
|
220
|
+
* @param fields
|
221
|
+
* an object with property names as keys and SQLite types as
|
222
|
+
* values, e.g. {name: "TEXT", age: "INT"}
|
223
|
+
* @return the entity's constructor
|
224
|
+
*/
|
225
|
+
persistence.define = function (entityName, fields) {
|
226
|
+
if (entityMeta[entityName]) { // Already defined, ignore
|
227
|
+
return getEntity(entityName);
|
228
|
+
}
|
229
|
+
var meta = {
|
230
|
+
name: entityName,
|
231
|
+
fields: fields,
|
232
|
+
isMixin: false,
|
233
|
+
indexes: [],
|
234
|
+
hasMany: {},
|
235
|
+
hasOne: {}
|
236
|
+
};
|
237
|
+
entityMeta[entityName] = meta;
|
238
|
+
return getEntity(entityName);
|
239
|
+
};
|
240
|
+
|
241
|
+
/**
|
242
|
+
* Checks whether an entity exists
|
243
|
+
*
|
244
|
+
* @param entityName
|
245
|
+
* the name of the entity (also the table name in the database)
|
246
|
+
* @return `true` if the entity exists, otherwise `false`
|
247
|
+
*/
|
248
|
+
persistence.isDefined = function (entityName) {
|
249
|
+
return !!entityMeta[entityName];
|
250
|
+
}
|
251
|
+
|
252
|
+
/**
|
253
|
+
* Define a mixin
|
254
|
+
*
|
255
|
+
* @param mixinName
|
256
|
+
* the name of the mixin
|
257
|
+
* @param fields
|
258
|
+
* an object with property names as keys and SQLite types as
|
259
|
+
* values, e.g. {name: "TEXT", age: "INT"}
|
260
|
+
* @return the entity's constructor
|
261
|
+
*/
|
262
|
+
persistence.defineMixin = function (mixinName, fields) {
|
263
|
+
var Entity = this.define(mixinName, fields);
|
264
|
+
Entity.meta.isMixin = true;
|
265
|
+
return Entity;
|
266
|
+
};
|
267
|
+
|
268
|
+
persistence.isTransaction = function(obj) {
|
269
|
+
return !obj || (obj && obj.executeSql);
|
270
|
+
};
|
271
|
+
|
272
|
+
persistence.isSession = function(obj) {
|
273
|
+
return !obj || (obj && obj.schemaSync);
|
274
|
+
};
|
275
|
+
|
276
|
+
/**
|
277
|
+
* Adds the object to tracked entities to be persisted
|
278
|
+
*
|
279
|
+
* @param obj
|
280
|
+
* the object to be tracked
|
281
|
+
*/
|
282
|
+
persistence.add = function (obj) {
|
283
|
+
if(!obj) return;
|
284
|
+
if (!this.trackedObjects[obj.id]) {
|
285
|
+
this.trackedObjects[obj.id] = obj;
|
286
|
+
if(obj._new) {
|
287
|
+
for(var p in obj._data) {
|
288
|
+
if(obj._data.hasOwnProperty(p)) {
|
289
|
+
this.propertyChanged(obj, p, undefined, obj._data[p]);
|
290
|
+
}
|
291
|
+
}
|
292
|
+
}
|
293
|
+
}
|
294
|
+
return this;
|
295
|
+
};
|
296
|
+
|
297
|
+
/**
|
298
|
+
* Marks the object to be removed (on next flush)
|
299
|
+
* @param obj object to be removed
|
300
|
+
*/
|
301
|
+
persistence.remove = function(obj) {
|
302
|
+
if (!this.objectsToRemove[obj.id]) {
|
303
|
+
this.objectsToRemove[obj.id] = obj;
|
304
|
+
}
|
305
|
+
this.objectsRemoved.push({id: obj.id, entity: obj._type});
|
306
|
+
this.objectRemoved(obj);
|
307
|
+
return this;
|
308
|
+
};
|
309
|
+
|
310
|
+
|
311
|
+
/**
|
312
|
+
* Clean the persistence context of cached entities and such.
|
313
|
+
*/
|
314
|
+
persistence.clean = function () {
|
315
|
+
this.trackedObjects = {};
|
316
|
+
this.objectsToRemove = {};
|
317
|
+
this.objectsRemoved = [];
|
318
|
+
this.globalPropertyListeners = {};
|
319
|
+
this.queryCollectionCache = {};
|
320
|
+
};
|
321
|
+
|
322
|
+
/**
|
323
|
+
* asynchronous sequential version of Array.prototype.forEach
|
324
|
+
* @param array the array to iterate over
|
325
|
+
* @param fn the function to apply to each item in the array, function
|
326
|
+
* has two argument, the first is the item value, the second a
|
327
|
+
* callback function
|
328
|
+
* @param callback the function to call when the forEach has ended
|
329
|
+
*/
|
330
|
+
persistence.asyncForEach = function(array, fn, callback) {
|
331
|
+
array = array.slice(0); // Just to be sure
|
332
|
+
function processOne() {
|
333
|
+
var item = array.pop();
|
334
|
+
fn(item, function(result, err) {
|
335
|
+
if(array.length > 0) {
|
336
|
+
processOne();
|
337
|
+
} else {
|
338
|
+
callback(result, err);
|
339
|
+
}
|
340
|
+
});
|
341
|
+
}
|
342
|
+
if(array.length > 0) {
|
343
|
+
processOne();
|
344
|
+
} else {
|
345
|
+
callback();
|
346
|
+
}
|
347
|
+
};
|
348
|
+
|
349
|
+
/**
|
350
|
+
* asynchronous parallel version of Array.prototype.forEach
|
351
|
+
* @param array the array to iterate over
|
352
|
+
* @param fn the function to apply to each item in the array, function
|
353
|
+
* has two argument, the first is the item value, the second a
|
354
|
+
* callback function
|
355
|
+
* @param callback the function to call when the forEach has ended
|
356
|
+
*/
|
357
|
+
persistence.asyncParForEach = function(array, fn, callback) {
|
358
|
+
var completed = 0;
|
359
|
+
var arLength = array.length;
|
360
|
+
if(arLength === 0) {
|
361
|
+
callback();
|
362
|
+
}
|
363
|
+
for(var i = 0; i < arLength; i++) {
|
364
|
+
fn(array[i], function(result, err) {
|
365
|
+
completed++;
|
366
|
+
if(completed === arLength) {
|
367
|
+
callback(result, err);
|
368
|
+
}
|
369
|
+
});
|
370
|
+
}
|
371
|
+
};
|
372
|
+
|
373
|
+
/**
|
374
|
+
* Retrieves or creates an entity constructor function for a given
|
375
|
+
* entity name
|
376
|
+
* @return the entity constructor function to be invoked with `new fn()`
|
377
|
+
*/
|
378
|
+
function getEntity(entityName) {
|
379
|
+
if (entityClassCache[entityName]) {
|
380
|
+
return entityClassCache[entityName];
|
381
|
+
}
|
382
|
+
var meta = entityMeta[entityName];
|
383
|
+
|
384
|
+
/**
|
385
|
+
* @constructor
|
386
|
+
*/
|
387
|
+
function Entity (session, obj, noEvents) {
|
388
|
+
var args = argspec.getArgs(arguments, [
|
389
|
+
{ name: "session", optional: true, check: persistence.isSession, defaultValue: persistence },
|
390
|
+
{ name: "obj", optional: true, check: function(obj) { return obj; }, defaultValue: {} }
|
391
|
+
]);
|
392
|
+
if (meta.isMixin)
|
393
|
+
throw new Error("Cannot instantiate mixin");
|
394
|
+
session = args.session;
|
395
|
+
obj = args.obj;
|
396
|
+
|
397
|
+
var that = this;
|
398
|
+
this.id = obj.id || persistence.createUUID();
|
399
|
+
this._new = true;
|
400
|
+
this._type = entityName;
|
401
|
+
this._dirtyProperties = {};
|
402
|
+
this._data = {};
|
403
|
+
this._data_obj = {}; // references to objects
|
404
|
+
this._session = session || persistence;
|
405
|
+
this.subscribers = {}; // observable
|
406
|
+
|
407
|
+
for ( var field in meta.fields) {
|
408
|
+
(function () {
|
409
|
+
if (meta.fields.hasOwnProperty(field)) {
|
410
|
+
var f = field; // Javascript scopes/closures SUCK
|
411
|
+
persistence.defineProp(that, f, function(val) {
|
412
|
+
// setterCallback
|
413
|
+
var oldValue = that._data[f];
|
414
|
+
if(oldValue !== val || (oldValue && val && oldValue.getTime && val.getTime)) { // Don't mark properties as dirty and trigger events unnecessarily
|
415
|
+
that._data[f] = val;
|
416
|
+
that._dirtyProperties[f] = oldValue;
|
417
|
+
that.triggerEvent('set', that, f, val);
|
418
|
+
that.triggerEvent('change', that, f, val);
|
419
|
+
session.propertyChanged(that, f, oldValue, val);
|
420
|
+
}
|
421
|
+
}, function() {
|
422
|
+
// getterCallback
|
423
|
+
return that._data[f];
|
424
|
+
});
|
425
|
+
that._data[field] = defaultValue(meta.fields[field]);
|
426
|
+
}
|
427
|
+
}());
|
428
|
+
}
|
429
|
+
|
430
|
+
for ( var it in meta.hasOne) {
|
431
|
+
if (meta.hasOne.hasOwnProperty(it)) {
|
432
|
+
(function () {
|
433
|
+
var ref = it;
|
434
|
+
var mixinClass = meta.hasOne[it].type.meta.isMixin ? ref + '_class' : null;
|
435
|
+
persistence.defineProp(that, ref, function(val) {
|
436
|
+
// setterCallback
|
437
|
+
var oldValue = that._data[ref];
|
438
|
+
var oldValueObj = that._data_obj[ref] || session.trackedObjects[that._data[ref]];
|
439
|
+
if (val == null) {
|
440
|
+
that._data[ref] = null;
|
441
|
+
that._data_obj[ref] = undefined;
|
442
|
+
if (mixinClass)
|
443
|
+
that[mixinClass] = '';
|
444
|
+
} else if (val.id) {
|
445
|
+
that._data[ref] = val.id;
|
446
|
+
that._data_obj[ref] = val;
|
447
|
+
if (mixinClass)
|
448
|
+
that[mixinClass] = val._type;
|
449
|
+
session.add(val);
|
450
|
+
session.add(that);
|
451
|
+
} else { // let's assume it's an id
|
452
|
+
that._data[ref] = val;
|
453
|
+
}
|
454
|
+
that._dirtyProperties[ref] = oldValue;
|
455
|
+
that.triggerEvent('set', that, ref, val);
|
456
|
+
that.triggerEvent('change', that, ref, val);
|
457
|
+
// Inverse
|
458
|
+
if(meta.hasOne[ref].inverseProperty) {
|
459
|
+
var newVal = that[ref];
|
460
|
+
if(newVal) {
|
461
|
+
var inverse = newVal[meta.hasOne[ref].inverseProperty];
|
462
|
+
if(inverse.list && inverse._filter) {
|
463
|
+
inverse.triggerEvent('change', that, ref, val);
|
464
|
+
}
|
465
|
+
}
|
466
|
+
if(oldValueObj) {
|
467
|
+
console.log("OldValue", oldValueObj);
|
468
|
+
var inverse = oldValueObj[meta.hasOne[ref].inverseProperty];
|
469
|
+
if(inverse.list && inverse._filter) {
|
470
|
+
inverse.triggerEvent('change', that, ref, val);
|
471
|
+
}
|
472
|
+
}
|
473
|
+
}
|
474
|
+
}, function() {
|
475
|
+
// getterCallback
|
476
|
+
if (!that._data[ref]) {
|
477
|
+
return null;
|
478
|
+
} else if(that._data_obj[ref] !== undefined) {
|
479
|
+
return that._data_obj[ref];
|
480
|
+
} else if(that._data[ref] && session.trackedObjects[that._data[ref]]) {
|
481
|
+
that._data_obj[ref] = session.trackedObjects[that._data[ref]];
|
482
|
+
return that._data_obj[ref];
|
483
|
+
} else {
|
484
|
+
throw new Error("Property '" + ref + "' of '" + meta.name + "' with id: " + that._data[ref] + " not fetched, either prefetch it or fetch it manually.");
|
485
|
+
}
|
486
|
+
});
|
487
|
+
}());
|
488
|
+
}
|
489
|
+
}
|
490
|
+
|
491
|
+
for ( var it in meta.hasMany) {
|
492
|
+
if (meta.hasMany.hasOwnProperty(it)) {
|
493
|
+
(function () {
|
494
|
+
var coll = it;
|
495
|
+
if (meta.hasMany[coll].manyToMany) {
|
496
|
+
persistence.defineProp(that, coll, function(val) {
|
497
|
+
// setterCallback
|
498
|
+
if(val && val._items) {
|
499
|
+
// Local query collection, just add each item
|
500
|
+
// TODO: this is technically not correct, should clear out existing items too
|
501
|
+
var items = val._items;
|
502
|
+
for(var i = 0; i < items.length; i++) {
|
503
|
+
persistence.get(that, coll).add(items[i]);
|
504
|
+
}
|
505
|
+
} else {
|
506
|
+
throw new Error("Not yet supported.");
|
507
|
+
}
|
508
|
+
}, function() {
|
509
|
+
// getterCallback
|
510
|
+
if (that._data[coll]) {
|
511
|
+
return that._data[coll];
|
512
|
+
} else {
|
513
|
+
var rel = meta.hasMany[coll];
|
514
|
+
var inverseMeta = rel.type.meta;
|
515
|
+
var inv = inverseMeta.hasMany[rel.inverseProperty];
|
516
|
+
var direct = rel.mixin ? rel.mixin.meta.name : meta.name;
|
517
|
+
var inverse = inv.mixin ? inv.mixin.meta.name : inverseMeta.name;
|
518
|
+
|
519
|
+
var queryColl = new persistence.ManyToManyDbQueryCollection(session, inverseMeta.name);
|
520
|
+
queryColl.initManyToMany(that, coll);
|
521
|
+
queryColl._manyToManyFetch = {
|
522
|
+
table: rel.tableName,
|
523
|
+
prop: direct + '_' + coll,
|
524
|
+
inverseProp: inverse + '_' + rel.inverseProperty,
|
525
|
+
id: that.id
|
526
|
+
};
|
527
|
+
that._data[coll] = queryColl;
|
528
|
+
return session.uniqueQueryCollection(queryColl);
|
529
|
+
}
|
530
|
+
});
|
531
|
+
} else { // one to many
|
532
|
+
persistence.defineProp(that, coll, function(val) {
|
533
|
+
// setterCallback
|
534
|
+
if(val && val._items) {
|
535
|
+
// Local query collection, just add each item
|
536
|
+
// TODO: this is technically not correct, should clear out existing items too
|
537
|
+
var items = val._items;
|
538
|
+
for(var i = 0; i < items.length; i++) {
|
539
|
+
persistence.get(that, coll).add(items[i]);
|
540
|
+
}
|
541
|
+
} else {
|
542
|
+
throw new Error("Not yet supported.");
|
543
|
+
}
|
544
|
+
}, function() {
|
545
|
+
// getterCallback
|
546
|
+
if (that._data[coll]) {
|
547
|
+
return that._data[coll];
|
548
|
+
} else {
|
549
|
+
var queryColl = session.uniqueQueryCollection(new persistence.DbQueryCollection(session, meta.hasMany[coll].type.meta.name).filter(meta.hasMany[coll].inverseProperty, '=', that));
|
550
|
+
that._data[coll] = queryColl;
|
551
|
+
return queryColl;
|
552
|
+
}
|
553
|
+
});
|
554
|
+
}
|
555
|
+
}());
|
556
|
+
}
|
557
|
+
}
|
558
|
+
|
559
|
+
if(this.initialize) {
|
560
|
+
this.initialize();
|
561
|
+
}
|
562
|
+
|
563
|
+
for ( var f in obj) {
|
564
|
+
if (obj.hasOwnProperty(f)) {
|
565
|
+
if(f !== 'id') {
|
566
|
+
persistence.set(that, f, obj[f]);
|
567
|
+
}
|
568
|
+
}
|
569
|
+
}
|
570
|
+
} // Entity
|
571
|
+
|
572
|
+
Entity.prototype = new Observable();
|
573
|
+
|
574
|
+
Entity.meta = meta;
|
575
|
+
|
576
|
+
Entity.prototype.equals = function(other) {
|
577
|
+
return this.id == other.id;
|
578
|
+
};
|
579
|
+
|
580
|
+
Entity.prototype.toJSON = function() {
|
581
|
+
var json = {id: this.id};
|
582
|
+
for(var p in this._data) {
|
583
|
+
if(this._data.hasOwnProperty(p)) {
|
584
|
+
if (typeof this._data[p] == "object" && this._data[p] != null) {
|
585
|
+
if (this._data[p].toJSON != undefined) {
|
586
|
+
json[p] = this._data[p].toJSON();
|
587
|
+
}
|
588
|
+
} else {
|
589
|
+
json[p] = this._data[p];
|
590
|
+
}
|
591
|
+
}
|
592
|
+
}
|
593
|
+
return json;
|
594
|
+
};
|
595
|
+
|
596
|
+
|
597
|
+
/**
|
598
|
+
* Select a subset of data as a JSON structure (Javascript object)
|
599
|
+
*
|
600
|
+
* A property specification is passed that selects the
|
601
|
+
* properties to be part of the resulting JSON object. Examples:
|
602
|
+
* ['id', 'name'] -> Will return an object with the id and name property of this entity
|
603
|
+
* ['*'] -> Will return an object with all the properties of this entity, not recursive
|
604
|
+
* ['project.name'] -> will return an object with a project property which has a name
|
605
|
+
* property containing the project name (hasOne relationship)
|
606
|
+
* ['project.[id, name]'] -> will return an object with a project property which has an
|
607
|
+
* id and name property containing the project name
|
608
|
+
* (hasOne relationship)
|
609
|
+
* ['tags.name'] -> will return an object with an array `tags` property containing
|
610
|
+
* objects each with a single property: name
|
611
|
+
*
|
612
|
+
* @param tx database transaction to use, leave out to start a new one
|
613
|
+
* @param props a property specification
|
614
|
+
* @param callback(result)
|
615
|
+
*/
|
616
|
+
Entity.prototype.selectJSON = function(tx, props, callback) {
|
617
|
+
var that = this;
|
618
|
+
var args = argspec.getArgs(arguments, [
|
619
|
+
{ name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null },
|
620
|
+
{ name: "props", optional: false },
|
621
|
+
{ name: "callback", optional: false }
|
622
|
+
]);
|
623
|
+
tx = args.tx;
|
624
|
+
props = args.props;
|
625
|
+
callback = args.callback;
|
626
|
+
|
627
|
+
if(!tx) {
|
628
|
+
this._session.transaction(function(tx) {
|
629
|
+
that.selectJSON(tx, props, callback);
|
630
|
+
});
|
631
|
+
return;
|
632
|
+
}
|
633
|
+
var includeProperties = {};
|
634
|
+
props.forEach(function(prop) {
|
635
|
+
var current = includeProperties;
|
636
|
+
var parts = prop.split('.');
|
637
|
+
for(var i = 0; i < parts.length; i++) {
|
638
|
+
var part = parts[i];
|
639
|
+
if(i === parts.length-1) {
|
640
|
+
if(part === '*') {
|
641
|
+
current.id = true;
|
642
|
+
for(var p in meta.fields) {
|
643
|
+
if(meta.fields.hasOwnProperty(p)) {
|
644
|
+
current[p] = true;
|
645
|
+
}
|
646
|
+
}
|
647
|
+
for(var p in meta.hasOne) {
|
648
|
+
if(meta.hasOne.hasOwnProperty(p)) {
|
649
|
+
current[p] = true;
|
650
|
+
}
|
651
|
+
}
|
652
|
+
for(var p in meta.hasMany) {
|
653
|
+
if(meta.hasMany.hasOwnProperty(p)) {
|
654
|
+
current[p] = true;
|
655
|
+
}
|
656
|
+
}
|
657
|
+
} else if(part[0] === '[') {
|
658
|
+
part = part.substring(1, part.length-1);
|
659
|
+
var propList = part.split(/,\s*/);
|
660
|
+
propList.forEach(function(prop) {
|
661
|
+
current[prop] = true;
|
662
|
+
});
|
663
|
+
} else {
|
664
|
+
current[part] = true;
|
665
|
+
}
|
666
|
+
} else {
|
667
|
+
current[part] = current[part] || {};
|
668
|
+
current = current[part];
|
669
|
+
}
|
670
|
+
}
|
671
|
+
});
|
672
|
+
buildJSON(this, tx, includeProperties, callback);
|
673
|
+
};
|
674
|
+
|
675
|
+
function buildJSON(that, tx, includeProperties, callback) {
|
676
|
+
var session = that._session;
|
677
|
+
var properties = [];
|
678
|
+
var meta = getMeta(that._type);
|
679
|
+
var fieldSpec = meta.fields;
|
680
|
+
|
681
|
+
for(var p in includeProperties) {
|
682
|
+
if(includeProperties.hasOwnProperty(p)) {
|
683
|
+
properties.push(p);
|
684
|
+
}
|
685
|
+
}
|
686
|
+
|
687
|
+
var cheapProperties = [];
|
688
|
+
var expensiveProperties = [];
|
689
|
+
|
690
|
+
properties.forEach(function(p) {
|
691
|
+
if(includeProperties[p] === true && !meta.hasMany[p]) { // simple, loaded field
|
692
|
+
cheapProperties.push(p);
|
693
|
+
} else {
|
694
|
+
expensiveProperties.push(p);
|
695
|
+
}
|
696
|
+
});
|
697
|
+
|
698
|
+
var itemData = that._data;
|
699
|
+
var item = {};
|
700
|
+
|
701
|
+
cheapProperties.forEach(function(p) {
|
702
|
+
if(p === 'id') {
|
703
|
+
item.id = that.id;
|
704
|
+
} else if(meta.hasOne[p]) {
|
705
|
+
item[p] = itemData[p] ? {id: itemData[p]} : null;
|
706
|
+
} else {
|
707
|
+
item[p] = persistence.entityValToJson(itemData[p], fieldSpec[p]);
|
708
|
+
}
|
709
|
+
});
|
710
|
+
properties = expensiveProperties.slice();
|
711
|
+
|
712
|
+
persistence.asyncForEach(properties, function(p, callback) {
|
713
|
+
if(meta.hasOne[p]) {
|
714
|
+
that.fetch(tx, p, function(obj) {
|
715
|
+
if(obj) {
|
716
|
+
buildJSON(obj, tx, includeProperties[p], function(result) {
|
717
|
+
item[p] = result;
|
718
|
+
callback();
|
719
|
+
});
|
720
|
+
} else {
|
721
|
+
item[p] = null;
|
722
|
+
callback();
|
723
|
+
}
|
724
|
+
});
|
725
|
+
} else if(meta.hasMany[p]) {
|
726
|
+
persistence.get(that, p).list(function(objs) {
|
727
|
+
item[p] = [];
|
728
|
+
persistence.asyncForEach(objs, function(obj, callback) {
|
729
|
+
var obj = objs.pop();
|
730
|
+
if(includeProperties[p] === true) {
|
731
|
+
item[p].push({id: obj.id});
|
732
|
+
callback();
|
733
|
+
} else {
|
734
|
+
buildJSON(obj, tx, includeProperties[p], function(result) {
|
735
|
+
item[p].push(result);
|
736
|
+
callback();
|
737
|
+
});
|
738
|
+
}
|
739
|
+
}, callback);
|
740
|
+
});
|
741
|
+
}
|
742
|
+
}, function() {
|
743
|
+
callback(item);
|
744
|
+
});
|
745
|
+
}; // End of buildJson
|
746
|
+
|
747
|
+
Entity.prototype.fetch = function(tx, rel, callback) {
|
748
|
+
var args = argspec.getArgs(arguments, [
|
749
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
750
|
+
{ name: 'rel', optional: false, check: argspec.hasType('string') },
|
751
|
+
{ name: 'callback', optional: false, check: argspec.isCallback() }
|
752
|
+
]);
|
753
|
+
tx = args.tx;
|
754
|
+
rel = args.rel;
|
755
|
+
callback = args.callback;
|
756
|
+
|
757
|
+
var that = this;
|
758
|
+
var session = this._session;
|
759
|
+
|
760
|
+
if(!tx) {
|
761
|
+
session.transaction(function(tx) {
|
762
|
+
that.fetch(tx, rel, callback);
|
763
|
+
});
|
764
|
+
return;
|
765
|
+
}
|
766
|
+
if(!this._data[rel]) { // null
|
767
|
+
if(callback) {
|
768
|
+
callback(null);
|
769
|
+
}
|
770
|
+
} else if(this._data_obj[rel]) { // already loaded
|
771
|
+
if(callback) {
|
772
|
+
callback(this._data_obj[rel]);
|
773
|
+
}
|
774
|
+
} else {
|
775
|
+
var type = meta.hasOne[rel].type;
|
776
|
+
if (type.meta.isMixin) {
|
777
|
+
type = getEntity(this._data[rel + '_class']);
|
778
|
+
}
|
779
|
+
type.load(session, tx, this._data[rel], function(obj) {
|
780
|
+
that._data_obj[rel] = obj;
|
781
|
+
if(callback) {
|
782
|
+
callback(obj);
|
783
|
+
}
|
784
|
+
});
|
785
|
+
}
|
786
|
+
};
|
787
|
+
|
788
|
+
/**
|
789
|
+
* Currently this is only required when changing JSON properties
|
790
|
+
*/
|
791
|
+
Entity.prototype.markDirty = function(prop) {
|
792
|
+
this._dirtyProperties[prop] = true;
|
793
|
+
};
|
794
|
+
|
795
|
+
/**
|
796
|
+
* Returns a QueryCollection implementation matching all instances
|
797
|
+
* of this entity in the database
|
798
|
+
*/
|
799
|
+
Entity.all = function(session) {
|
800
|
+
var args = argspec.getArgs(arguments, [
|
801
|
+
{ name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }
|
802
|
+
]);
|
803
|
+
session = args.session;
|
804
|
+
return session.uniqueQueryCollection(new AllDbQueryCollection(session, entityName));
|
805
|
+
};
|
806
|
+
|
807
|
+
Entity.fromSelectJSON = function(session, tx, jsonObj, callback) {
|
808
|
+
var args = argspec.getArgs(arguments, [
|
809
|
+
{ name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence },
|
810
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
811
|
+
{ name: 'jsonObj', optional: false },
|
812
|
+
{ name: 'callback', optional: false, check: argspec.isCallback() }
|
813
|
+
]);
|
814
|
+
session = args.session;
|
815
|
+
tx = args.tx;
|
816
|
+
jsonObj = args.jsonObj;
|
817
|
+
callback = args.callback;
|
818
|
+
|
819
|
+
if(!tx) {
|
820
|
+
session.transaction(function(tx) {
|
821
|
+
Entity.fromSelectJSON(session, tx, jsonObj, callback);
|
822
|
+
});
|
823
|
+
return;
|
824
|
+
}
|
825
|
+
|
826
|
+
if(typeof jsonObj === 'string') {
|
827
|
+
jsonObj = JSON.parse(jsonObj);
|
828
|
+
}
|
829
|
+
|
830
|
+
if(!jsonObj) {
|
831
|
+
callback(null);
|
832
|
+
return;
|
833
|
+
}
|
834
|
+
|
835
|
+
function loadedObj(obj) {
|
836
|
+
if(!obj) {
|
837
|
+
obj = new Entity(session);
|
838
|
+
if(jsonObj.id) {
|
839
|
+
obj.id = jsonObj.id;
|
840
|
+
}
|
841
|
+
}
|
842
|
+
session.add(obj);
|
843
|
+
var expensiveProperties = [];
|
844
|
+
for(var p in jsonObj) {
|
845
|
+
if(jsonObj.hasOwnProperty(p)) {
|
846
|
+
if(p === 'id') {
|
847
|
+
continue;
|
848
|
+
} else if(meta.fields[p]) { // regular field
|
849
|
+
persistence.set(obj, p, persistence.jsonToEntityVal(jsonObj[p], meta.fields[p]));
|
850
|
+
} else if(meta.hasOne[p] || meta.hasMany[p]){
|
851
|
+
expensiveProperties.push(p);
|
852
|
+
}
|
853
|
+
}
|
854
|
+
}
|
855
|
+
persistence.asyncForEach(expensiveProperties, function(p, callback) {
|
856
|
+
if(meta.hasOne[p]) {
|
857
|
+
meta.hasOne[p].type.fromSelectJSON(session, tx, jsonObj[p], function(result) {
|
858
|
+
persistence.set(obj, p, result);
|
859
|
+
callback();
|
860
|
+
});
|
861
|
+
} else if(meta.hasMany[p]) {
|
862
|
+
var coll = persistence.get(obj, p);
|
863
|
+
var ar = jsonObj[p].slice(0);
|
864
|
+
var PropertyEntity = meta.hasMany[p].type;
|
865
|
+
// get all current items
|
866
|
+
coll.list(tx, function(currentItems) {
|
867
|
+
persistence.asyncForEach(ar, function(item, callback) {
|
868
|
+
PropertyEntity.fromSelectJSON(session, tx, item, function(result) {
|
869
|
+
// Check if not already in collection
|
870
|
+
for(var i = 0; i < currentItems.length; i++) {
|
871
|
+
if(currentItems[i].id === result.id) {
|
872
|
+
callback();
|
873
|
+
return;
|
874
|
+
}
|
875
|
+
}
|
876
|
+
coll.add(result);
|
877
|
+
callback();
|
878
|
+
});
|
879
|
+
}, function() {
|
880
|
+
callback();
|
881
|
+
});
|
882
|
+
});
|
883
|
+
}
|
884
|
+
}, function() {
|
885
|
+
callback(obj);
|
886
|
+
});
|
887
|
+
}
|
888
|
+
if(jsonObj.id) {
|
889
|
+
Entity.load(session, tx, jsonObj.id, loadedObj);
|
890
|
+
} else {
|
891
|
+
loadedObj(new Entity(session));
|
892
|
+
}
|
893
|
+
};
|
894
|
+
|
895
|
+
Entity.load = function(session, tx, id, callback) {
|
896
|
+
var args = argspec.getArgs(arguments, [
|
897
|
+
{ name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence },
|
898
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
899
|
+
{ name: 'id', optional: false, check: argspec.hasType('string') },
|
900
|
+
{ name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} }
|
901
|
+
]);
|
902
|
+
Entity.findBy(args.session, args.tx, "id", args.id, args.callback);
|
903
|
+
};
|
904
|
+
|
905
|
+
Entity.findBy = function(session, tx, property, value, callback) {
|
906
|
+
var args = argspec.getArgs(arguments, [
|
907
|
+
{ name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence },
|
908
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
909
|
+
{ name: 'property', optional: false, check: argspec.hasType('string') },
|
910
|
+
{ name: 'value', optional: false },
|
911
|
+
{ name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} }
|
912
|
+
]);
|
913
|
+
session = args.session;
|
914
|
+
tx = args.tx;
|
915
|
+
property = args.property;
|
916
|
+
value = args.value;
|
917
|
+
callback = args.callback;
|
918
|
+
|
919
|
+
if(property === 'id' && value in session.trackedObjects) {
|
920
|
+
callback(session.trackedObjects[value]);
|
921
|
+
return;
|
922
|
+
}
|
923
|
+
if(!tx) {
|
924
|
+
session.transaction(function(tx) {
|
925
|
+
Entity.findBy(session, tx, property, value, callback);
|
926
|
+
});
|
927
|
+
return;
|
928
|
+
}
|
929
|
+
Entity.all(session).filter(property, "=", value).one(tx, function(obj) {
|
930
|
+
callback(obj);
|
931
|
+
});
|
932
|
+
}
|
933
|
+
|
934
|
+
|
935
|
+
Entity.index = function(cols,options) {
|
936
|
+
var opts = options || {};
|
937
|
+
if (typeof cols=="string") {
|
938
|
+
cols = [cols];
|
939
|
+
}
|
940
|
+
opts.columns = cols;
|
941
|
+
meta.indexes.push(opts);
|
942
|
+
};
|
943
|
+
|
944
|
+
/**
|
945
|
+
* Declares a one-to-many or many-to-many relationship to another entity
|
946
|
+
* Whether 1:N or N:M is chosed depends on the inverse declaration
|
947
|
+
* @param collName the name of the collection (becomes a property of
|
948
|
+
* Entity instances
|
949
|
+
* @param otherEntity the constructor function of the entity to define
|
950
|
+
* the relation to
|
951
|
+
* @param inverseRel the name of the inverse property (to be) defined on otherEntity
|
952
|
+
*/
|
953
|
+
Entity.hasMany = function (collName, otherEntity, invRel) {
|
954
|
+
var otherMeta = otherEntity.meta;
|
955
|
+
if (otherMeta.hasMany[invRel]) {
|
956
|
+
// other side has declared it as a one-to-many relation too -> it's in
|
957
|
+
// fact many-to-many
|
958
|
+
var tableName = meta.name + "_" + collName + "_" + otherMeta.name;
|
959
|
+
var inverseTableName = otherMeta.name + '_' + invRel + '_' + meta.name;
|
960
|
+
|
961
|
+
if (tableName > inverseTableName) {
|
962
|
+
// Some arbitrary way to deterministically decide which table to generate
|
963
|
+
tableName = inverseTableName;
|
964
|
+
}
|
965
|
+
meta.hasMany[collName] = {
|
966
|
+
type: otherEntity,
|
967
|
+
inverseProperty: invRel,
|
968
|
+
manyToMany: true,
|
969
|
+
tableName: tableName
|
970
|
+
};
|
971
|
+
otherMeta.hasMany[invRel] = {
|
972
|
+
type: Entity,
|
973
|
+
inverseProperty: collName,
|
974
|
+
manyToMany: true,
|
975
|
+
tableName: tableName
|
976
|
+
};
|
977
|
+
delete meta.hasOne[collName];
|
978
|
+
delete meta.fields[collName + "_class"]; // in case it existed
|
979
|
+
} else {
|
980
|
+
meta.hasMany[collName] = {
|
981
|
+
type: otherEntity,
|
982
|
+
inverseProperty: invRel
|
983
|
+
};
|
984
|
+
otherMeta.hasOne[invRel] = {
|
985
|
+
type: Entity,
|
986
|
+
inverseProperty: collName
|
987
|
+
};
|
988
|
+
if (meta.isMixin)
|
989
|
+
otherMeta.fields[invRel + "_class"] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT";
|
990
|
+
}
|
991
|
+
}
|
992
|
+
|
993
|
+
Entity.hasOne = function (refName, otherEntity, inverseProperty) {
|
994
|
+
meta.hasOne[refName] = {
|
995
|
+
type: otherEntity,
|
996
|
+
inverseProperty: inverseProperty
|
997
|
+
};
|
998
|
+
if (otherEntity.meta.isMixin)
|
999
|
+
meta.fields[refName + "_class"] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT";
|
1000
|
+
};
|
1001
|
+
|
1002
|
+
Entity.is = function(mixin){
|
1003
|
+
var mixinMeta = mixin.meta;
|
1004
|
+
if (!mixinMeta.isMixin)
|
1005
|
+
throw new Error("not a mixin: " + mixin);
|
1006
|
+
|
1007
|
+
mixin.meta.mixedIns = mixin.meta.mixedIns || [];
|
1008
|
+
mixin.meta.mixedIns.push(meta);
|
1009
|
+
|
1010
|
+
for (var field in mixinMeta.fields) {
|
1011
|
+
if (mixinMeta.fields.hasOwnProperty(field))
|
1012
|
+
meta.fields[field] = mixinMeta.fields[field];
|
1013
|
+
}
|
1014
|
+
for (var it in mixinMeta.hasOne) {
|
1015
|
+
if (mixinMeta.hasOne.hasOwnProperty(it))
|
1016
|
+
meta.hasOne[it] = mixinMeta.hasOne[it];
|
1017
|
+
}
|
1018
|
+
for (var it in mixinMeta.hasMany) {
|
1019
|
+
if (mixinMeta.hasMany.hasOwnProperty(it)) {
|
1020
|
+
mixinMeta.hasMany[it].mixin = mixin;
|
1021
|
+
meta.hasMany[it] = mixinMeta.hasMany[it];
|
1022
|
+
}
|
1023
|
+
}
|
1024
|
+
}
|
1025
|
+
|
1026
|
+
// Allow decorator functions to add more stuff
|
1027
|
+
var fns = persistence.entityDecoratorHooks;
|
1028
|
+
for(var i = 0; i < fns.length; i++) {
|
1029
|
+
fns[i](Entity);
|
1030
|
+
}
|
1031
|
+
|
1032
|
+
entityClassCache[entityName] = Entity;
|
1033
|
+
return Entity;
|
1034
|
+
}
|
1035
|
+
|
1036
|
+
persistence.jsonToEntityVal = function(value, type) {
|
1037
|
+
if(type) {
|
1038
|
+
switch(type) {
|
1039
|
+
case 'DATE':
|
1040
|
+
if(typeof value === 'number') {
|
1041
|
+
if (value > 1000000000000) {
|
1042
|
+
// it's in milliseconds
|
1043
|
+
return new Date(value);
|
1044
|
+
} else {
|
1045
|
+
return new Date(value * 1000);
|
1046
|
+
}
|
1047
|
+
} else {
|
1048
|
+
return null;
|
1049
|
+
}
|
1050
|
+
break;
|
1051
|
+
default:
|
1052
|
+
return value;
|
1053
|
+
}
|
1054
|
+
} else {
|
1055
|
+
return value;
|
1056
|
+
}
|
1057
|
+
};
|
1058
|
+
|
1059
|
+
persistence.entityValToJson = function(value, type) {
|
1060
|
+
if(type) {
|
1061
|
+
switch(type) {
|
1062
|
+
case 'DATE':
|
1063
|
+
if(value) {
|
1064
|
+
value = new Date(value);
|
1065
|
+
return Math.round(value.getTime() / 1000);
|
1066
|
+
} else {
|
1067
|
+
return null;
|
1068
|
+
}
|
1069
|
+
break;
|
1070
|
+
default:
|
1071
|
+
return value;
|
1072
|
+
}
|
1073
|
+
} else {
|
1074
|
+
return value;
|
1075
|
+
}
|
1076
|
+
};
|
1077
|
+
|
1078
|
+
/**
|
1079
|
+
* Dumps the entire database into an object (that can be serialized to JSON for instance)
|
1080
|
+
* @param tx transaction to use, use `null` to start a new one
|
1081
|
+
* @param entities a list of entity constructor functions to serialize, use `null` for all
|
1082
|
+
* @param callback (object) the callback function called with the results.
|
1083
|
+
*/
|
1084
|
+
persistence.dump = function(tx, entities, callback) {
|
1085
|
+
var args = argspec.getArgs(arguments, [
|
1086
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
1087
|
+
{ name: 'entities', optional: true, check: function(obj) { return !obj || (obj && obj.length && !obj.apply); }, defaultValue: null },
|
1088
|
+
{ name: 'callback', optional: false, check: argspec.isCallback(), defaultValue: function(){} }
|
1089
|
+
]);
|
1090
|
+
tx = args.tx;
|
1091
|
+
entities = args.entities;
|
1092
|
+
callback = args.callback;
|
1093
|
+
|
1094
|
+
if(!entities) { // Default: all entity types
|
1095
|
+
entities = [];
|
1096
|
+
for(var e in entityClassCache) {
|
1097
|
+
if(entityClassCache.hasOwnProperty(e)) {
|
1098
|
+
entities.push(entityClassCache[e]);
|
1099
|
+
}
|
1100
|
+
}
|
1101
|
+
}
|
1102
|
+
|
1103
|
+
var result = {};
|
1104
|
+
persistence.asyncParForEach(entities, function(Entity, callback) {
|
1105
|
+
Entity.all().list(tx, function(all) {
|
1106
|
+
var items = [];
|
1107
|
+
persistence.asyncParForEach(all, function(e, callback) {
|
1108
|
+
var rec = {};
|
1109
|
+
var fields = Entity.meta.fields;
|
1110
|
+
for(var f in fields) {
|
1111
|
+
if(fields.hasOwnProperty(f)) {
|
1112
|
+
rec[f] = persistence.entityValToJson(e._data[f], fields[f]);
|
1113
|
+
}
|
1114
|
+
}
|
1115
|
+
var refs = Entity.meta.hasOne;
|
1116
|
+
for(var r in refs) {
|
1117
|
+
if(refs.hasOwnProperty(r)) {
|
1118
|
+
rec[r] = e._data[r];
|
1119
|
+
}
|
1120
|
+
}
|
1121
|
+
var colls = Entity.meta.hasMany;
|
1122
|
+
var collArray = [];
|
1123
|
+
for(var coll in colls) {
|
1124
|
+
if(colls.hasOwnProperty(coll)) {
|
1125
|
+
collArray.push(coll);
|
1126
|
+
}
|
1127
|
+
}
|
1128
|
+
persistence.asyncParForEach(collArray, function(collP, callback) {
|
1129
|
+
var coll = persistence.get(e, collP);
|
1130
|
+
coll.list(tx, function(results) {
|
1131
|
+
rec[collP] = results.map(function(r) { return r.id; });
|
1132
|
+
callback();
|
1133
|
+
});
|
1134
|
+
}, function() {
|
1135
|
+
rec.id = e.id;
|
1136
|
+
items.push(rec);
|
1137
|
+
callback();
|
1138
|
+
});
|
1139
|
+
}, function() {
|
1140
|
+
result[Entity.meta.name] = items;
|
1141
|
+
callback();
|
1142
|
+
});
|
1143
|
+
});
|
1144
|
+
}, function() {
|
1145
|
+
callback(result);
|
1146
|
+
});
|
1147
|
+
};
|
1148
|
+
|
1149
|
+
/**
|
1150
|
+
* Loads a set of entities from a dump object
|
1151
|
+
* @param tx transaction to use, use `null` to start a new one
|
1152
|
+
* @param dump the dump object
|
1153
|
+
* @param callback the callback function called when done.
|
1154
|
+
*/
|
1155
|
+
persistence.load = function(tx, dump, callback) {
|
1156
|
+
var args = argspec.getArgs(arguments, [
|
1157
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
1158
|
+
{ name: 'dump', optional: false },
|
1159
|
+
{ name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} }
|
1160
|
+
]);
|
1161
|
+
tx = args.tx;
|
1162
|
+
dump = args.dump;
|
1163
|
+
callback = args.callback;
|
1164
|
+
|
1165
|
+
var finishedCount = 0;
|
1166
|
+
var collItemsToAdd = [];
|
1167
|
+
var session = this;
|
1168
|
+
for(var entityName in dump) {
|
1169
|
+
if(dump.hasOwnProperty(entityName)) {
|
1170
|
+
var Entity = getEntity(entityName);
|
1171
|
+
var fields = Entity.meta.fields;
|
1172
|
+
var instances = dump[entityName];
|
1173
|
+
for(var i = 0; i < instances.length; i++) {
|
1174
|
+
var instance = instances[i];
|
1175
|
+
var ent = new Entity();
|
1176
|
+
ent.id = instance.id;
|
1177
|
+
for(var p in instance) {
|
1178
|
+
if(instance.hasOwnProperty(p)) {
|
1179
|
+
if (persistence.isImmutable(p)) {
|
1180
|
+
ent[p] = instance[p];
|
1181
|
+
} else if(Entity.meta.hasMany[p]) { // collection
|
1182
|
+
var many = Entity.meta.hasMany[p];
|
1183
|
+
if(many.manyToMany && Entity.meta.name < many.type.meta.name) { // Arbitrary way to avoid double adding
|
1184
|
+
continue;
|
1185
|
+
}
|
1186
|
+
var coll = persistence.get(ent, p);
|
1187
|
+
if(instance[p].length > 0) {
|
1188
|
+
instance[p].forEach(function(it) {
|
1189
|
+
collItemsToAdd.push({Entity: Entity, coll: coll, id: it});
|
1190
|
+
});
|
1191
|
+
}
|
1192
|
+
} else {
|
1193
|
+
persistence.set(ent, p, persistence.jsonToEntityVal(instance[p], fields[p]));
|
1194
|
+
}
|
1195
|
+
}
|
1196
|
+
}
|
1197
|
+
this.add(ent);
|
1198
|
+
}
|
1199
|
+
}
|
1200
|
+
}
|
1201
|
+
session.flush(tx, function() {
|
1202
|
+
persistence.asyncForEach(collItemsToAdd, function(collItem, callback) {
|
1203
|
+
collItem.Entity.load(session, tx, collItem.id, function(obj) {
|
1204
|
+
collItem.coll.add(obj);
|
1205
|
+
callback();
|
1206
|
+
});
|
1207
|
+
}, function() {
|
1208
|
+
session.flush(tx, callback);
|
1209
|
+
});
|
1210
|
+
});
|
1211
|
+
};
|
1212
|
+
|
1213
|
+
/**
|
1214
|
+
* Dumps the entire database to a JSON string
|
1215
|
+
* @param tx transaction to use, use `null` to start a new one
|
1216
|
+
* @param entities a list of entity constructor functions to serialize, use `null` for all
|
1217
|
+
* @param callback (jsonDump) the callback function called with the results.
|
1218
|
+
*/
|
1219
|
+
persistence.dumpToJson = function(tx, entities, callback) {
|
1220
|
+
var args = argspec.getArgs(arguments, [
|
1221
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
1222
|
+
{ name: 'entities', optional: true, check: function(obj) { return obj && obj.length && !obj.apply; }, defaultValue: null },
|
1223
|
+
{ name: 'callback', optional: false, check: argspec.isCallback(), defaultValue: function(){} }
|
1224
|
+
]);
|
1225
|
+
tx = args.tx;
|
1226
|
+
entities = args.entities;
|
1227
|
+
callback = args.callback;
|
1228
|
+
this.dump(tx, entities, function(obj) {
|
1229
|
+
callback(JSON.stringify(obj));
|
1230
|
+
});
|
1231
|
+
};
|
1232
|
+
|
1233
|
+
/**
|
1234
|
+
* Loads data from a JSON string (as dumped by `dumpToJson`)
|
1235
|
+
* @param tx transaction to use, use `null` to start a new one
|
1236
|
+
* @param jsonDump JSON string
|
1237
|
+
* @param callback the callback function called when done.
|
1238
|
+
*/
|
1239
|
+
persistence.loadFromJson = function(tx, jsonDump, callback) {
|
1240
|
+
var args = argspec.getArgs(arguments, [
|
1241
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
1242
|
+
{ name: 'jsonDump', optional: false },
|
1243
|
+
{ name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} }
|
1244
|
+
]);
|
1245
|
+
tx = args.tx;
|
1246
|
+
jsonDump = args.jsonDump;
|
1247
|
+
callback = args.callback;
|
1248
|
+
this.load(tx, JSON.parse(jsonDump), callback);
|
1249
|
+
};
|
1250
|
+
|
1251
|
+
|
1252
|
+
/**
|
1253
|
+
* Generates a UUID according to http://www.ietf.org/rfc/rfc4122.txt
|
1254
|
+
*/
|
1255
|
+
function createUUID () {
|
1256
|
+
if(persistence.typeMapper && persistence.typeMapper.newUuid) {
|
1257
|
+
return persistence.typeMapper.newUuid();
|
1258
|
+
}
|
1259
|
+
var s = [];
|
1260
|
+
var hexDigits = "0123456789ABCDEF";
|
1261
|
+
for ( var i = 0; i < 32; i++) {
|
1262
|
+
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
|
1263
|
+
}
|
1264
|
+
s[12] = "4";
|
1265
|
+
s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1);
|
1266
|
+
|
1267
|
+
var uuid = s.join("");
|
1268
|
+
return uuid;
|
1269
|
+
}
|
1270
|
+
|
1271
|
+
persistence.createUUID = createUUID;
|
1272
|
+
|
1273
|
+
|
1274
|
+
function defaultValue(type) {
|
1275
|
+
if(persistence.typeMapper && persistence.typeMapper.defaultValue) {
|
1276
|
+
return persistence.typeMapper.defaultValue(type);
|
1277
|
+
}
|
1278
|
+
switch(type) {
|
1279
|
+
case "TEXT": return "";
|
1280
|
+
case "BOOL": return false;
|
1281
|
+
default:
|
1282
|
+
if(type.indexOf("INT") !== -1) {
|
1283
|
+
return 0;
|
1284
|
+
} else if(type.indexOf("CHAR") !== -1) {
|
1285
|
+
return "";
|
1286
|
+
} else {
|
1287
|
+
return null;
|
1288
|
+
}
|
1289
|
+
}
|
1290
|
+
}
|
1291
|
+
|
1292
|
+
function arrayContains(ar, item) {
|
1293
|
+
var l = ar.length;
|
1294
|
+
for(var i = 0; i < l; i++) {
|
1295
|
+
var el = ar[i];
|
1296
|
+
if(el.equals && el.equals(item)) {
|
1297
|
+
return true;
|
1298
|
+
} else if(el === item) {
|
1299
|
+
return true;
|
1300
|
+
}
|
1301
|
+
}
|
1302
|
+
return false;
|
1303
|
+
}
|
1304
|
+
|
1305
|
+
function arrayRemove(ar, item) {
|
1306
|
+
var l = ar.length;
|
1307
|
+
for(var i = 0; i < l; i++) {
|
1308
|
+
var el = ar[i];
|
1309
|
+
if(el.equals && el.equals(item)) {
|
1310
|
+
ar.splice(i, 1);
|
1311
|
+
return;
|
1312
|
+
} else if(el === item) {
|
1313
|
+
ar.splice(i, 1);
|
1314
|
+
return;
|
1315
|
+
}
|
1316
|
+
}
|
1317
|
+
}
|
1318
|
+
|
1319
|
+
////////////////// QUERY COLLECTIONS \\\\\\\\\\\\\\\\\\\\\\\
|
1320
|
+
|
1321
|
+
function Subscription(obj, eventType, fn) {
|
1322
|
+
this.obj = obj;
|
1323
|
+
this.eventType = eventType;
|
1324
|
+
this.fn = fn;
|
1325
|
+
}
|
1326
|
+
|
1327
|
+
Subscription.prototype.unsubscribe = function() {
|
1328
|
+
this.obj.removeEventListener(this.eventType, this.fn);
|
1329
|
+
};
|
1330
|
+
|
1331
|
+
/**
|
1332
|
+
* Simple observable function constructor
|
1333
|
+
* @constructor
|
1334
|
+
*/
|
1335
|
+
function Observable() {
|
1336
|
+
this.subscribers = {};
|
1337
|
+
}
|
1338
|
+
|
1339
|
+
Observable.prototype.addEventListener = function (eventType, fn) {
|
1340
|
+
if (!this.subscribers[eventType]) {
|
1341
|
+
this.subscribers[eventType] = [];
|
1342
|
+
}
|
1343
|
+
this.subscribers[eventType].push(fn);
|
1344
|
+
return new Subscription(this, eventType, fn);
|
1345
|
+
};
|
1346
|
+
|
1347
|
+
Observable.prototype.removeEventListener = function(eventType, fn) {
|
1348
|
+
var subscribers = this.subscribers[eventType];
|
1349
|
+
for ( var i = 0; i < subscribers.length; i++) {
|
1350
|
+
if(subscribers[i] == fn) {
|
1351
|
+
this.subscribers[eventType].splice(i, 1);
|
1352
|
+
return true;
|
1353
|
+
}
|
1354
|
+
}
|
1355
|
+
return false;
|
1356
|
+
};
|
1357
|
+
|
1358
|
+
Observable.prototype.triggerEvent = function (eventType) {
|
1359
|
+
if (!this.subscribers[eventType]) { // No subscribers to this event type
|
1360
|
+
return;
|
1361
|
+
}
|
1362
|
+
var subscribers = this.subscribers[eventType].slice(0);
|
1363
|
+
for(var i = 0; i < subscribers.length; i++) {
|
1364
|
+
subscribers[i].apply(null, arguments);
|
1365
|
+
}
|
1366
|
+
};
|
1367
|
+
|
1368
|
+
/*
|
1369
|
+
* Each filter has 4 methods:
|
1370
|
+
* - sql(prefix, values) -- returns a SQL representation of this filter,
|
1371
|
+
* possibly pushing additional query arguments to `values` if ?'s are used
|
1372
|
+
* in the query
|
1373
|
+
* - match(o) -- returns whether the filter matches the object o.
|
1374
|
+
* - makeFit(o) -- attempts to adapt the object o in such a way that it matches
|
1375
|
+
* this filter.
|
1376
|
+
* - makeNotFit(o) -- the oppositive of makeFit, makes the object o NOT match
|
1377
|
+
* this filter
|
1378
|
+
*/
|
1379
|
+
|
1380
|
+
/**
|
1381
|
+
* Default filter that does not filter on anything
|
1382
|
+
* currently it generates a 1=1 SQL query, which is kind of ugly
|
1383
|
+
*/
|
1384
|
+
function NullFilter () {
|
1385
|
+
}
|
1386
|
+
|
1387
|
+
NullFilter.prototype.match = function (o) {
|
1388
|
+
return true;
|
1389
|
+
};
|
1390
|
+
|
1391
|
+
NullFilter.prototype.makeFit = function(o) {
|
1392
|
+
};
|
1393
|
+
|
1394
|
+
NullFilter.prototype.makeNotFit = function(o) {
|
1395
|
+
};
|
1396
|
+
|
1397
|
+
NullFilter.prototype.toUniqueString = function() {
|
1398
|
+
return "NULL";
|
1399
|
+
};
|
1400
|
+
|
1401
|
+
NullFilter.prototype.subscribeGlobally = function() { };
|
1402
|
+
|
1403
|
+
NullFilter.prototype.unsubscribeGlobally = function() { };
|
1404
|
+
|
1405
|
+
/**
|
1406
|
+
* Filter that makes sure that both its left and right filter match
|
1407
|
+
* @param left left-hand filter object
|
1408
|
+
* @param right right-hand filter object
|
1409
|
+
*/
|
1410
|
+
function AndFilter (left, right) {
|
1411
|
+
this.left = left;
|
1412
|
+
this.right = right;
|
1413
|
+
}
|
1414
|
+
|
1415
|
+
AndFilter.prototype.match = function (o) {
|
1416
|
+
return this.left.match(o) && this.right.match(o);
|
1417
|
+
};
|
1418
|
+
|
1419
|
+
AndFilter.prototype.makeFit = function(o) {
|
1420
|
+
this.left.makeFit(o);
|
1421
|
+
this.right.makeFit(o);
|
1422
|
+
};
|
1423
|
+
|
1424
|
+
AndFilter.prototype.makeNotFit = function(o) {
|
1425
|
+
this.left.makeNotFit(o);
|
1426
|
+
this.right.makeNotFit(o);
|
1427
|
+
};
|
1428
|
+
|
1429
|
+
AndFilter.prototype.toUniqueString = function() {
|
1430
|
+
return this.left.toUniqueString() + " AND " + this.right.toUniqueString();
|
1431
|
+
};
|
1432
|
+
|
1433
|
+
AndFilter.prototype.subscribeGlobally = function(coll, entityName) {
|
1434
|
+
this.left.subscribeGlobally(coll, entityName);
|
1435
|
+
this.right.subscribeGlobally(coll, entityName);
|
1436
|
+
};
|
1437
|
+
|
1438
|
+
AndFilter.prototype.unsubscribeGlobally = function(coll, entityName) {
|
1439
|
+
this.left.unsubscribeGlobally(coll, entityName);
|
1440
|
+
this.right.unsubscribeGlobally(coll, entityName);
|
1441
|
+
};
|
1442
|
+
|
1443
|
+
/**
|
1444
|
+
* Filter that makes sure that either its left and right filter match
|
1445
|
+
* @param left left-hand filter object
|
1446
|
+
* @param right right-hand filter object
|
1447
|
+
*/
|
1448
|
+
function OrFilter (left, right) {
|
1449
|
+
this.left = left;
|
1450
|
+
this.right = right;
|
1451
|
+
}
|
1452
|
+
|
1453
|
+
OrFilter.prototype.match = function (o) {
|
1454
|
+
return this.left.match(o) || this.right.match(o);
|
1455
|
+
};
|
1456
|
+
|
1457
|
+
OrFilter.prototype.makeFit = function(o) {
|
1458
|
+
this.left.makeFit(o);
|
1459
|
+
this.right.makeFit(o);
|
1460
|
+
};
|
1461
|
+
|
1462
|
+
OrFilter.prototype.makeNotFit = function(o) {
|
1463
|
+
this.left.makeNotFit(o);
|
1464
|
+
this.right.makeNotFit(o);
|
1465
|
+
};
|
1466
|
+
|
1467
|
+
OrFilter.prototype.toUniqueString = function() {
|
1468
|
+
return this.left.toUniqueString() + " OR " + this.right.toUniqueString();
|
1469
|
+
};
|
1470
|
+
|
1471
|
+
OrFilter.prototype.subscribeGlobally = function(coll, entityName) {
|
1472
|
+
this.left.subscribeGlobally(coll, entityName);
|
1473
|
+
this.right.subscribeGlobally(coll, entityName);
|
1474
|
+
};
|
1475
|
+
|
1476
|
+
OrFilter.prototype.unsubscribeGlobally = function(coll, entityName) {
|
1477
|
+
this.left.unsubscribeGlobally(coll, entityName);
|
1478
|
+
this.right.unsubscribeGlobally(coll, entityName);
|
1479
|
+
};
|
1480
|
+
|
1481
|
+
/**
|
1482
|
+
* Filter that checks whether a certain property matches some value, based on an
|
1483
|
+
* operator. Supported operators are '=', '!=', '<', '<=', '>' and '>='.
|
1484
|
+
* @param property the property name
|
1485
|
+
* @param operator the operator to compare with
|
1486
|
+
* @param value the literal value to compare to
|
1487
|
+
*/
|
1488
|
+
function PropertyFilter (property, operator, value) {
|
1489
|
+
this.property = property;
|
1490
|
+
this.operator = operator.toLowerCase();
|
1491
|
+
this.value = value;
|
1492
|
+
}
|
1493
|
+
|
1494
|
+
PropertyFilter.prototype.match = function (o) {
|
1495
|
+
var value = this.value;
|
1496
|
+
var propValue = persistence.get(o, this.property);
|
1497
|
+
if(value && value.getTime) { // DATE
|
1498
|
+
// TODO: Deal with arrays of dates for 'in' and 'not in'
|
1499
|
+
value = Math.round(value.getTime() / 1000) * 1000; // Deal with precision
|
1500
|
+
if(propValue && propValue.getTime) { // DATE
|
1501
|
+
propValue = Math.round(propValue.getTime() / 1000) * 1000; // Deal with precision
|
1502
|
+
}
|
1503
|
+
}
|
1504
|
+
switch (this.operator) {
|
1505
|
+
case '=':
|
1506
|
+
return propValue === value;
|
1507
|
+
break;
|
1508
|
+
case '!=':
|
1509
|
+
return propValue !== value;
|
1510
|
+
break;
|
1511
|
+
case '<':
|
1512
|
+
return propValue < value;
|
1513
|
+
break;
|
1514
|
+
case '<=':
|
1515
|
+
return propValue <= value;
|
1516
|
+
break;
|
1517
|
+
case '>':
|
1518
|
+
return propValue > value;
|
1519
|
+
break;
|
1520
|
+
case '>=':
|
1521
|
+
return propValue >= value;
|
1522
|
+
break;
|
1523
|
+
case 'in':
|
1524
|
+
return arrayContains(value, propValue);
|
1525
|
+
break;
|
1526
|
+
case 'not in':
|
1527
|
+
return !arrayContains(value, propValue);
|
1528
|
+
break;
|
1529
|
+
}
|
1530
|
+
};
|
1531
|
+
|
1532
|
+
PropertyFilter.prototype.makeFit = function(o) {
|
1533
|
+
if(this.operator === '=') {
|
1534
|
+
persistence.set(o, this.property, this.value);
|
1535
|
+
} else {
|
1536
|
+
throw new Error("Sorry, can't perform makeFit for other filters than =");
|
1537
|
+
}
|
1538
|
+
};
|
1539
|
+
|
1540
|
+
PropertyFilter.prototype.makeNotFit = function(o) {
|
1541
|
+
if(this.operator === '=') {
|
1542
|
+
persistence.set(o, this.property, null);
|
1543
|
+
} else {
|
1544
|
+
throw new Error("Sorry, can't perform makeNotFit for other filters than =");
|
1545
|
+
}
|
1546
|
+
};
|
1547
|
+
|
1548
|
+
PropertyFilter.prototype.subscribeGlobally = function(coll, entityName) {
|
1549
|
+
persistence.subscribeToGlobalPropertyListener(coll, entityName, this.property);
|
1550
|
+
};
|
1551
|
+
|
1552
|
+
PropertyFilter.prototype.unsubscribeGlobally = function(coll, entityName) {
|
1553
|
+
persistence.unsubscribeFromGlobalPropertyListener(coll, entityName, this.property);
|
1554
|
+
};
|
1555
|
+
|
1556
|
+
PropertyFilter.prototype.toUniqueString = function() {
|
1557
|
+
var val = this.value;
|
1558
|
+
if(val && val._type) {
|
1559
|
+
val = val.id;
|
1560
|
+
}
|
1561
|
+
return this.property + this.operator + val;
|
1562
|
+
};
|
1563
|
+
|
1564
|
+
persistence.NullFilter = NullFilter;
|
1565
|
+
persistence.AndFilter = AndFilter;
|
1566
|
+
persistence.OrFilter = OrFilter;
|
1567
|
+
persistence.PropertyFilter = PropertyFilter;
|
1568
|
+
|
1569
|
+
/**
|
1570
|
+
* Ensure global uniqueness of query collection object
|
1571
|
+
*/
|
1572
|
+
persistence.uniqueQueryCollection = function(coll) {
|
1573
|
+
var entityName = coll._entityName;
|
1574
|
+
if(coll._items) { // LocalQueryCollection
|
1575
|
+
return coll;
|
1576
|
+
}
|
1577
|
+
if(!this.queryCollectionCache[entityName]) {
|
1578
|
+
this.queryCollectionCache[entityName] = {};
|
1579
|
+
}
|
1580
|
+
var uniqueString = coll.toUniqueString();
|
1581
|
+
if(!this.queryCollectionCache[entityName][uniqueString]) {
|
1582
|
+
this.queryCollectionCache[entityName][uniqueString] = coll;
|
1583
|
+
}
|
1584
|
+
return this.queryCollectionCache[entityName][uniqueString];
|
1585
|
+
}
|
1586
|
+
|
1587
|
+
/**
|
1588
|
+
* The constructor function of the _abstract_ QueryCollection
|
1589
|
+
* DO NOT INSTANTIATE THIS
|
1590
|
+
* @constructor
|
1591
|
+
*/
|
1592
|
+
function QueryCollection () {
|
1593
|
+
}
|
1594
|
+
|
1595
|
+
QueryCollection.prototype = new Observable();
|
1596
|
+
|
1597
|
+
QueryCollection.prototype.oldAddEventListener = QueryCollection.prototype.addEventListener;
|
1598
|
+
|
1599
|
+
QueryCollection.prototype.setupSubscriptions = function() {
|
1600
|
+
this._filter.subscribeGlobally(this, this._entityName);
|
1601
|
+
};
|
1602
|
+
|
1603
|
+
QueryCollection.prototype.teardownSubscriptions = function() {
|
1604
|
+
this._filter.unsubscribeGlobally(this, this._entityName);
|
1605
|
+
};
|
1606
|
+
|
1607
|
+
QueryCollection.prototype.addEventListener = function(eventType, fn) {
|
1608
|
+
var that = this;
|
1609
|
+
var subscription = this.oldAddEventListener(eventType, fn);
|
1610
|
+
if(this.subscribers[eventType].length === 1) { // first subscriber
|
1611
|
+
this.setupSubscriptions();
|
1612
|
+
}
|
1613
|
+
subscription.oldUnsubscribe = subscription.unsubscribe;
|
1614
|
+
subscription.unsubscribe = function() {
|
1615
|
+
this.oldUnsubscribe();
|
1616
|
+
|
1617
|
+
if(that.subscribers[eventType].length === 0) { // last subscriber
|
1618
|
+
that.teardownSubscriptions();
|
1619
|
+
}
|
1620
|
+
};
|
1621
|
+
return subscription;
|
1622
|
+
};
|
1623
|
+
|
1624
|
+
/**
|
1625
|
+
* Function called when session is flushed, returns list of SQL queries to execute
|
1626
|
+
* (as [query, arg] tuples)
|
1627
|
+
*/
|
1628
|
+
QueryCollection.prototype.persistQueries = function() { return []; };
|
1629
|
+
|
1630
|
+
/**
|
1631
|
+
* Invoked by sub-classes to initialize the query collection
|
1632
|
+
*/
|
1633
|
+
QueryCollection.prototype.init = function (session, entityName, constructor) {
|
1634
|
+
this._filter = new NullFilter();
|
1635
|
+
this._orderColumns = []; // tuples of [column, ascending]
|
1636
|
+
this._prefetchFields = [];
|
1637
|
+
this._entityName = entityName;
|
1638
|
+
this._constructor = constructor;
|
1639
|
+
this._limit = -1;
|
1640
|
+
this._skip = 0;
|
1641
|
+
this._reverse = false;
|
1642
|
+
this._session = session || persistence;
|
1643
|
+
// For observable
|
1644
|
+
this.subscribers = {};
|
1645
|
+
}
|
1646
|
+
|
1647
|
+
QueryCollection.prototype.toUniqueString = function() {
|
1648
|
+
var s = this._constructor.name + ": " + this._entityName;
|
1649
|
+
s += '|Filter:';
|
1650
|
+
var values = [];
|
1651
|
+
s += this._filter.toUniqueString();
|
1652
|
+
s += '|Values:';
|
1653
|
+
for(var i = 0; i < values.length; i++) {
|
1654
|
+
s += values + "|^|";
|
1655
|
+
}
|
1656
|
+
s += '|Order:';
|
1657
|
+
for(var i = 0; i < this._orderColumns.length; i++) {
|
1658
|
+
var col = this._orderColumns[i];
|
1659
|
+
s += col[0] + ", " + col[1] + ", " + col[2];
|
1660
|
+
}
|
1661
|
+
s += '|Prefetch:';
|
1662
|
+
for(var i = 0; i < this._prefetchFields.length; i++) {
|
1663
|
+
s += this._prefetchFields[i];
|
1664
|
+
}
|
1665
|
+
s += '|Limit:';
|
1666
|
+
s += this._limit;
|
1667
|
+
s += '|Skip:';
|
1668
|
+
s += this._skip;
|
1669
|
+
s += '|Reverse:';
|
1670
|
+
s += this._reverse;
|
1671
|
+
return s;
|
1672
|
+
};
|
1673
|
+
|
1674
|
+
/**
|
1675
|
+
* Creates a clone of this query collection
|
1676
|
+
* @return a clone of the collection
|
1677
|
+
*/
|
1678
|
+
QueryCollection.prototype.clone = function (cloneSubscribers) {
|
1679
|
+
var c = new (this._constructor)(this._session, this._entityName);
|
1680
|
+
c._filter = this._filter;
|
1681
|
+
c._prefetchFields = this._prefetchFields.slice(0); // clone
|
1682
|
+
c._orderColumns = this._orderColumns.slice(0);
|
1683
|
+
c._limit = this._limit;
|
1684
|
+
c._skip = this._skip;
|
1685
|
+
c._reverse = this._reverse;
|
1686
|
+
if(cloneSubscribers) {
|
1687
|
+
var subscribers = {};
|
1688
|
+
for(var eventType in this.subscribers) {
|
1689
|
+
if(this.subscribers.hasOwnProperty(eventType)) {
|
1690
|
+
subscribers[eventType] = this.subscribers[eventType].slice(0);
|
1691
|
+
}
|
1692
|
+
}
|
1693
|
+
c.subscribers = subscribers; //this.subscribers;
|
1694
|
+
} else {
|
1695
|
+
c.subscribers = this.subscribers;
|
1696
|
+
}
|
1697
|
+
return c;
|
1698
|
+
};
|
1699
|
+
|
1700
|
+
/**
|
1701
|
+
* Returns a new query collection with a property filter condition added
|
1702
|
+
* @param property the property to filter on
|
1703
|
+
* @param operator the operator to use
|
1704
|
+
* @param value the literal value that the property should match
|
1705
|
+
* @return the query collection with the filter added
|
1706
|
+
*/
|
1707
|
+
QueryCollection.prototype.filter = function (property, operator, value) {
|
1708
|
+
var c = this.clone(true);
|
1709
|
+
c._filter = new AndFilter(this._filter, new PropertyFilter(property,
|
1710
|
+
operator, value));
|
1711
|
+
// Add global listener (TODO: memory leak waiting to happen!)
|
1712
|
+
var session = this._session;
|
1713
|
+
c = session.uniqueQueryCollection(c);
|
1714
|
+
//session.subscribeToGlobalPropertyListener(c, this._entityName, property);
|
1715
|
+
return session.uniqueQueryCollection(c);
|
1716
|
+
};
|
1717
|
+
|
1718
|
+
/**
|
1719
|
+
* Returns a new query collection with an OR condition between the
|
1720
|
+
* current filter and the filter specified as argument
|
1721
|
+
* @param filter the other filter
|
1722
|
+
* @return the new query collection
|
1723
|
+
*/
|
1724
|
+
QueryCollection.prototype.or = function (filter) {
|
1725
|
+
var c = this.clone(true);
|
1726
|
+
c._filter = new OrFilter(this._filter, filter);
|
1727
|
+
return this._session.uniqueQueryCollection(c);
|
1728
|
+
};
|
1729
|
+
|
1730
|
+
/**
|
1731
|
+
* Returns a new query collection with an AND condition between the
|
1732
|
+
* current filter and the filter specified as argument
|
1733
|
+
* @param filter the other filter
|
1734
|
+
* @return the new query collection
|
1735
|
+
*/
|
1736
|
+
QueryCollection.prototype.and = function (filter) {
|
1737
|
+
var c = this.clone(true);
|
1738
|
+
c._filter = new AndFilter(this._filter, filter);
|
1739
|
+
return this._session.uniqueQueryCollection(c);
|
1740
|
+
};
|
1741
|
+
|
1742
|
+
/**
|
1743
|
+
* Returns a new query collection with an ordering imposed on the collection
|
1744
|
+
* @param property the property to sort on
|
1745
|
+
* @param ascending should the order be ascending (= true) or descending (= false)
|
1746
|
+
* @param caseSensitive should the order be case sensitive (= true) or case insensitive (= false)
|
1747
|
+
* note: using case insensitive ordering for anything other than TEXT fields yields
|
1748
|
+
* undefinded behavior
|
1749
|
+
* @return the query collection with imposed ordering
|
1750
|
+
*/
|
1751
|
+
QueryCollection.prototype.order = function (property, ascending, caseSensitive) {
|
1752
|
+
ascending = ascending === undefined ? true : ascending;
|
1753
|
+
caseSensitive = caseSensitive === undefined ? true : caseSensitive;
|
1754
|
+
var c = this.clone();
|
1755
|
+
c._orderColumns.push( [ property, ascending, caseSensitive ]);
|
1756
|
+
return this._session.uniqueQueryCollection(c);
|
1757
|
+
};
|
1758
|
+
|
1759
|
+
/**
|
1760
|
+
* Returns a new query collection will limit its size to n items
|
1761
|
+
* @param n the number of items to limit it to
|
1762
|
+
* @return the limited query collection
|
1763
|
+
*/
|
1764
|
+
QueryCollection.prototype.limit = function(n) {
|
1765
|
+
var c = this.clone();
|
1766
|
+
c._limit = n;
|
1767
|
+
return this._session.uniqueQueryCollection(c);
|
1768
|
+
};
|
1769
|
+
|
1770
|
+
/**
|
1771
|
+
* Returns a new query collection which will skip the first n results
|
1772
|
+
* @param n the number of results to skip
|
1773
|
+
* @return the query collection that will skip n items
|
1774
|
+
*/
|
1775
|
+
QueryCollection.prototype.skip = function(n) {
|
1776
|
+
var c = this.clone();
|
1777
|
+
c._skip = n;
|
1778
|
+
return this._session.uniqueQueryCollection(c);
|
1779
|
+
};
|
1780
|
+
|
1781
|
+
/**
|
1782
|
+
* Returns a new query collection which reverse the order of the result set
|
1783
|
+
* @return the query collection that will reverse its items
|
1784
|
+
*/
|
1785
|
+
QueryCollection.prototype.reverse = function() {
|
1786
|
+
var c = this.clone();
|
1787
|
+
c._reverse = true;
|
1788
|
+
return this._session.uniqueQueryCollection(c);
|
1789
|
+
};
|
1790
|
+
|
1791
|
+
/**
|
1792
|
+
* Returns a new query collection which will prefetch a certain object relationship.
|
1793
|
+
* Only works with 1:1 and N:1 relations.
|
1794
|
+
* Relation must target an entity, not a mix-in.
|
1795
|
+
* @param rel the relation name of the relation to prefetch
|
1796
|
+
* @return the query collection prefetching `rel`
|
1797
|
+
*/
|
1798
|
+
QueryCollection.prototype.prefetch = function (rel) {
|
1799
|
+
var c = this.clone();
|
1800
|
+
c._prefetchFields.push(rel);
|
1801
|
+
return this._session.uniqueQueryCollection(c);
|
1802
|
+
};
|
1803
|
+
|
1804
|
+
|
1805
|
+
/**
|
1806
|
+
* Select a subset of data, represented by this query collection as a JSON
|
1807
|
+
* structure (Javascript object)
|
1808
|
+
*
|
1809
|
+
* @param tx database transaction to use, leave out to start a new one
|
1810
|
+
* @param props a property specification
|
1811
|
+
* @param callback(result)
|
1812
|
+
*/
|
1813
|
+
QueryCollection.prototype.selectJSON = function(tx, props, callback) {
|
1814
|
+
var args = argspec.getArgs(arguments, [
|
1815
|
+
{ name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null },
|
1816
|
+
{ name: "props", optional: false },
|
1817
|
+
{ name: "callback", optional: false }
|
1818
|
+
]);
|
1819
|
+
var session = this._session;
|
1820
|
+
var that = this;
|
1821
|
+
tx = args.tx;
|
1822
|
+
props = args.props;
|
1823
|
+
callback = args.callback;
|
1824
|
+
|
1825
|
+
if(!tx) {
|
1826
|
+
session.transaction(function(tx) {
|
1827
|
+
that.selectJSON(tx, props, callback);
|
1828
|
+
});
|
1829
|
+
return;
|
1830
|
+
}
|
1831
|
+
var Entity = getEntity(this._entityName);
|
1832
|
+
// TODO: This could do some clever prefetching to make it more efficient
|
1833
|
+
this.list(function(items) {
|
1834
|
+
var resultArray = [];
|
1835
|
+
persistence.asyncForEach(items, function(item, callback) {
|
1836
|
+
item.selectJSON(tx, props, function(obj) {
|
1837
|
+
resultArray.push(obj);
|
1838
|
+
callback();
|
1839
|
+
});
|
1840
|
+
}, function() {
|
1841
|
+
callback(resultArray);
|
1842
|
+
});
|
1843
|
+
});
|
1844
|
+
};
|
1845
|
+
|
1846
|
+
/**
|
1847
|
+
* Adds an object to a collection
|
1848
|
+
* @param obj the object to add
|
1849
|
+
*/
|
1850
|
+
QueryCollection.prototype.add = function(obj) {
|
1851
|
+
if(!obj.id || !obj._type) {
|
1852
|
+
throw new Error("Cannot add object of non-entity type onto collection.");
|
1853
|
+
}
|
1854
|
+
this._session.add(obj);
|
1855
|
+
this._filter.makeFit(obj);
|
1856
|
+
this.triggerEvent('add', this, obj);
|
1857
|
+
this.triggerEvent('change', this, obj);
|
1858
|
+
}
|
1859
|
+
|
1860
|
+
/**
|
1861
|
+
* Adds an an array of objects to a collection
|
1862
|
+
* @param obj the object to add
|
1863
|
+
*/
|
1864
|
+
QueryCollection.prototype.addAll = function(objs) {
|
1865
|
+
for(var i = 0; i < objs.length; i++) {
|
1866
|
+
var obj = objs[i];
|
1867
|
+
this._session.add(obj);
|
1868
|
+
this._filter.makeFit(obj);
|
1869
|
+
this.triggerEvent('add', this, obj);
|
1870
|
+
}
|
1871
|
+
this.triggerEvent('change', this);
|
1872
|
+
}
|
1873
|
+
|
1874
|
+
/**
|
1875
|
+
* Removes an object from a collection
|
1876
|
+
* @param obj the object to remove from the collection
|
1877
|
+
*/
|
1878
|
+
QueryCollection.prototype.remove = function(obj) {
|
1879
|
+
if(!obj.id || !obj._type) {
|
1880
|
+
throw new Error("Cannot remove object of non-entity type from collection.");
|
1881
|
+
}
|
1882
|
+
this._filter.makeNotFit(obj);
|
1883
|
+
this.triggerEvent('remove', this, obj);
|
1884
|
+
this.triggerEvent('change', this, obj);
|
1885
|
+
}
|
1886
|
+
|
1887
|
+
|
1888
|
+
/**
|
1889
|
+
* A database implementation of the QueryCollection
|
1890
|
+
* @param entityName the name of the entity to create the collection for
|
1891
|
+
* @constructor
|
1892
|
+
*/
|
1893
|
+
function DbQueryCollection (session, entityName) {
|
1894
|
+
this.init(session, entityName, DbQueryCollection);
|
1895
|
+
}
|
1896
|
+
|
1897
|
+
/**
|
1898
|
+
* Execute a function for each item in the list
|
1899
|
+
* @param tx the transaction to use (or null to open a new one)
|
1900
|
+
* @param eachFn (elem) the function to be executed for each item
|
1901
|
+
*/
|
1902
|
+
QueryCollection.prototype.each = function (tx, eachFn) {
|
1903
|
+
var args = argspec.getArgs(arguments, [
|
1904
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
1905
|
+
{ name: 'eachFn', optional: true, check: argspec.isCallback() }
|
1906
|
+
]);
|
1907
|
+
tx = args.tx;
|
1908
|
+
eachFn = args.eachFn;
|
1909
|
+
|
1910
|
+
this.list(tx, function(results) {
|
1911
|
+
for(var i = 0; i < results.length; i++) {
|
1912
|
+
eachFn(results[i]);
|
1913
|
+
}
|
1914
|
+
});
|
1915
|
+
}
|
1916
|
+
|
1917
|
+
// Alias
|
1918
|
+
QueryCollection.prototype.forEach = QueryCollection.prototype.each;
|
1919
|
+
|
1920
|
+
QueryCollection.prototype.one = function (tx, oneFn) {
|
1921
|
+
var args = argspec.getArgs(arguments, [
|
1922
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
1923
|
+
{ name: 'oneFn', optional: false, check: argspec.isCallback() }
|
1924
|
+
]);
|
1925
|
+
tx = args.tx;
|
1926
|
+
oneFn = args.oneFn;
|
1927
|
+
|
1928
|
+
var that = this;
|
1929
|
+
|
1930
|
+
this.limit(1).list(tx, function(results) {
|
1931
|
+
if(results.length === 0) {
|
1932
|
+
oneFn(null);
|
1933
|
+
} else {
|
1934
|
+
oneFn(results[0]);
|
1935
|
+
}
|
1936
|
+
});
|
1937
|
+
}
|
1938
|
+
|
1939
|
+
DbQueryCollection.prototype = new QueryCollection();
|
1940
|
+
|
1941
|
+
|
1942
|
+
/**
|
1943
|
+
* An implementation of QueryCollection, that is used
|
1944
|
+
* to represent all instances of an entity type
|
1945
|
+
* @constructor
|
1946
|
+
*/
|
1947
|
+
function AllDbQueryCollection (session, entityName) {
|
1948
|
+
this.init(session, entityName, AllDbQueryCollection);
|
1949
|
+
}
|
1950
|
+
|
1951
|
+
AllDbQueryCollection.prototype = new DbQueryCollection();
|
1952
|
+
|
1953
|
+
AllDbQueryCollection.prototype.add = function(obj) {
|
1954
|
+
this._session.add(obj);
|
1955
|
+
this.triggerEvent('add', this, obj);
|
1956
|
+
this.triggerEvent('change', this, obj);
|
1957
|
+
};
|
1958
|
+
|
1959
|
+
AllDbQueryCollection.prototype.remove = function(obj) {
|
1960
|
+
this._session.remove(obj);
|
1961
|
+
this.triggerEvent('remove', this, obj);
|
1962
|
+
this.triggerEvent('change', this, obj);
|
1963
|
+
};
|
1964
|
+
|
1965
|
+
/**
|
1966
|
+
* A ManyToMany implementation of QueryCollection
|
1967
|
+
* @constructor
|
1968
|
+
*/
|
1969
|
+
function ManyToManyDbQueryCollection (session, entityName) {
|
1970
|
+
this.init(session, entityName, persistence.ManyToManyDbQueryCollection);
|
1971
|
+
this._localAdded = [];
|
1972
|
+
this._localRemoved = [];
|
1973
|
+
}
|
1974
|
+
|
1975
|
+
ManyToManyDbQueryCollection.prototype = new DbQueryCollection();
|
1976
|
+
|
1977
|
+
ManyToManyDbQueryCollection.prototype.initManyToMany = function(obj, coll) {
|
1978
|
+
this._obj = obj;
|
1979
|
+
this._coll = coll;
|
1980
|
+
};
|
1981
|
+
|
1982
|
+
ManyToManyDbQueryCollection.prototype.add = function(obj) {
|
1983
|
+
if(!arrayContains(this._localAdded, obj)) {
|
1984
|
+
this._session.add(obj);
|
1985
|
+
this._localAdded.push(obj);
|
1986
|
+
this.triggerEvent('add', this, obj);
|
1987
|
+
this.triggerEvent('change', this, obj);
|
1988
|
+
}
|
1989
|
+
};
|
1990
|
+
|
1991
|
+
ManyToManyDbQueryCollection.prototype.addAll = function(objs) {
|
1992
|
+
for(var i = 0; i < objs.length; i++) {
|
1993
|
+
var obj = objs[i];
|
1994
|
+
if(!arrayContains(this._localAdded, obj)) {
|
1995
|
+
this._session.add(obj);
|
1996
|
+
this._localAdded.push(obj);
|
1997
|
+
this.triggerEvent('add', this, obj);
|
1998
|
+
}
|
1999
|
+
}
|
2000
|
+
this.triggerEvent('change', this);
|
2001
|
+
}
|
2002
|
+
|
2003
|
+
ManyToManyDbQueryCollection.prototype.clone = function() {
|
2004
|
+
var c = DbQueryCollection.prototype.clone.call(this);
|
2005
|
+
c._localAdded = this._localAdded;
|
2006
|
+
c._localRemoved = this._localRemoved;
|
2007
|
+
c._obj = this._obj;
|
2008
|
+
c._coll = this._coll;
|
2009
|
+
return c;
|
2010
|
+
};
|
2011
|
+
|
2012
|
+
ManyToManyDbQueryCollection.prototype.remove = function(obj) {
|
2013
|
+
if(arrayContains(this._localAdded, obj)) { // added locally, can just remove it from there
|
2014
|
+
arrayRemove(this._localAdded, obj);
|
2015
|
+
} else if(!arrayContains(this._localRemoved, obj)) {
|
2016
|
+
this._localRemoved.push(obj);
|
2017
|
+
}
|
2018
|
+
this.triggerEvent('remove', this, obj);
|
2019
|
+
this.triggerEvent('change', this, obj);
|
2020
|
+
};
|
2021
|
+
|
2022
|
+
////////// Local implementation of QueryCollection \\\\\\\\\\\\\\\\
|
2023
|
+
|
2024
|
+
function LocalQueryCollection(initialArray) {
|
2025
|
+
this.init(persistence, null, LocalQueryCollection);
|
2026
|
+
this._items = initialArray || [];
|
2027
|
+
}
|
2028
|
+
|
2029
|
+
LocalQueryCollection.prototype = new QueryCollection();
|
2030
|
+
|
2031
|
+
LocalQueryCollection.prototype.clone = function() {
|
2032
|
+
var c = DbQueryCollection.prototype.clone.call(this);
|
2033
|
+
c._items = this._items;
|
2034
|
+
return c;
|
2035
|
+
};
|
2036
|
+
|
2037
|
+
LocalQueryCollection.prototype.add = function(obj) {
|
2038
|
+
if(!arrayContains(this._items, obj)) {
|
2039
|
+
this._session.add(obj);
|
2040
|
+
this._items.push(obj);
|
2041
|
+
this.triggerEvent('add', this, obj);
|
2042
|
+
this.triggerEvent('change', this, obj);
|
2043
|
+
}
|
2044
|
+
};
|
2045
|
+
|
2046
|
+
LocalQueryCollection.prototype.addAll = function(objs) {
|
2047
|
+
for(var i = 0; i < objs.length; i++) {
|
2048
|
+
var obj = objs[i];
|
2049
|
+
if(!arrayContains(this._items, obj)) {
|
2050
|
+
this._session.add(obj);
|
2051
|
+
this._items.push(obj);
|
2052
|
+
this.triggerEvent('add', this, obj);
|
2053
|
+
}
|
2054
|
+
}
|
2055
|
+
this.triggerEvent('change', this);
|
2056
|
+
}
|
2057
|
+
|
2058
|
+
LocalQueryCollection.prototype.remove = function(obj) {
|
2059
|
+
var items = this._items;
|
2060
|
+
for(var i = 0; i < items.length; i++) {
|
2061
|
+
if(items[i] === obj) {
|
2062
|
+
this._items.splice(i, 1);
|
2063
|
+
this.triggerEvent('remove', this, obj);
|
2064
|
+
this.triggerEvent('change', this, obj);
|
2065
|
+
}
|
2066
|
+
}
|
2067
|
+
};
|
2068
|
+
|
2069
|
+
LocalQueryCollection.prototype.list = function(tx, callback) {
|
2070
|
+
var args = argspec.getArgs(arguments, [
|
2071
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
2072
|
+
{ name: 'callback', optional: true, check: argspec.isCallback() }
|
2073
|
+
]);
|
2074
|
+
callback = args.callback;
|
2075
|
+
|
2076
|
+
if(!callback || callback.executeSql) { // first argument is transaction
|
2077
|
+
callback = arguments[1]; // set to second argument
|
2078
|
+
}
|
2079
|
+
var array = this._items.slice(0);
|
2080
|
+
var that = this;
|
2081
|
+
var results = [];
|
2082
|
+
for(var i = 0; i < array.length; i++) {
|
2083
|
+
if(this._filter.match(array[i])) {
|
2084
|
+
results.push(array[i]);
|
2085
|
+
}
|
2086
|
+
}
|
2087
|
+
results.sort(function(a, b) {
|
2088
|
+
for(var i = 0; i < that._orderColumns.length; i++) {
|
2089
|
+
var col = that._orderColumns[i][0];
|
2090
|
+
var asc = that._orderColumns[i][1];
|
2091
|
+
var sens = that._orderColumns[i][2];
|
2092
|
+
var aVal = persistence.get(a, col);
|
2093
|
+
var bVal = persistence.get(b, col);
|
2094
|
+
if (!sens) {
|
2095
|
+
aVal = aVal.toLowerCase();
|
2096
|
+
bVal = bVal.toLowerCase();
|
2097
|
+
}
|
2098
|
+
if(aVal < bVal) {
|
2099
|
+
return asc ? -1 : 1;
|
2100
|
+
} else if(aVal > bVal) {
|
2101
|
+
return asc ? 1 : -1;
|
2102
|
+
}
|
2103
|
+
}
|
2104
|
+
return 0;
|
2105
|
+
});
|
2106
|
+
if(this._skip) {
|
2107
|
+
results.splice(0, this._skip);
|
2108
|
+
}
|
2109
|
+
if(this._limit > -1) {
|
2110
|
+
results = results.slice(0, this._limit);
|
2111
|
+
}
|
2112
|
+
if(this._reverse) {
|
2113
|
+
results.reverse();
|
2114
|
+
}
|
2115
|
+
if(callback) {
|
2116
|
+
callback(results);
|
2117
|
+
} else {
|
2118
|
+
return results;
|
2119
|
+
}
|
2120
|
+
};
|
2121
|
+
|
2122
|
+
LocalQueryCollection.prototype.destroyAll = function(callback) {
|
2123
|
+
if(!callback || callback.executeSql) { // first argument is transaction
|
2124
|
+
callback = arguments[1]; // set to second argument
|
2125
|
+
}
|
2126
|
+
this._items = [];
|
2127
|
+
this.triggerEvent('change', this);
|
2128
|
+
if(callback) callback();
|
2129
|
+
};
|
2130
|
+
|
2131
|
+
LocalQueryCollection.prototype.count = function(tx, callback) {
|
2132
|
+
var args = argspec.getArgs(arguments, [
|
2133
|
+
{ name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
|
2134
|
+
{ name: 'callback', optional: true, check: argspec.isCallback() }
|
2135
|
+
]);
|
2136
|
+
tx = args.tx;
|
2137
|
+
callback = args.callback;
|
2138
|
+
|
2139
|
+
var result = this.list();
|
2140
|
+
|
2141
|
+
if(callback) {
|
2142
|
+
callback(result.length);
|
2143
|
+
} else {
|
2144
|
+
return result.length;
|
2145
|
+
}
|
2146
|
+
};
|
2147
|
+
|
2148
|
+
persistence.QueryCollection = QueryCollection;
|
2149
|
+
persistence.DbQueryCollection = DbQueryCollection;
|
2150
|
+
persistence.ManyToManyDbQueryCollection = ManyToManyDbQueryCollection;
|
2151
|
+
persistence.LocalQueryCollection = LocalQueryCollection;
|
2152
|
+
persistence.Observable = Observable;
|
2153
|
+
persistence.Subscription = Subscription;
|
2154
|
+
persistence.AndFilter = AndFilter;
|
2155
|
+
persistence.OrFilter = OrFilter;
|
2156
|
+
persistence.PropertyFilter = PropertyFilter;
|
2157
|
+
}());
|
2158
|
+
|
2159
|
+
// ArgSpec.js library: http://github.com/zefhemel/argspecjs
|
2160
|
+
var argspec = {};
|
2161
|
+
|
2162
|
+
(function() {
|
2163
|
+
argspec.getArgs = function(args, specs) {
|
2164
|
+
var argIdx = 0;
|
2165
|
+
var specIdx = 0;
|
2166
|
+
var argObj = {};
|
2167
|
+
while(specIdx < specs.length) {
|
2168
|
+
var s = specs[specIdx];
|
2169
|
+
var a = args[argIdx];
|
2170
|
+
if(s.optional) {
|
2171
|
+
if(a !== undefined && s.check(a)) {
|
2172
|
+
argObj[s.name] = a;
|
2173
|
+
argIdx++;
|
2174
|
+
specIdx++;
|
2175
|
+
} else {
|
2176
|
+
if(s.defaultValue !== undefined) {
|
2177
|
+
argObj[s.name] = s.defaultValue;
|
2178
|
+
}
|
2179
|
+
specIdx++;
|
2180
|
+
}
|
2181
|
+
} else {
|
2182
|
+
if(s.check && !s.check(a)) {
|
2183
|
+
throw new Error("Invalid value for argument: " + s.name + " Value: " + a);
|
2184
|
+
}
|
2185
|
+
argObj[s.name] = a;
|
2186
|
+
specIdx++;
|
2187
|
+
argIdx++;
|
2188
|
+
}
|
2189
|
+
}
|
2190
|
+
return argObj;
|
2191
|
+
}
|
2192
|
+
|
2193
|
+
argspec.hasProperty = function(name) {
|
2194
|
+
return function(obj) {
|
2195
|
+
return obj && obj[name] !== undefined;
|
2196
|
+
};
|
2197
|
+
}
|
2198
|
+
|
2199
|
+
argspec.hasType = function(type) {
|
2200
|
+
return function(obj) {
|
2201
|
+
return typeof obj === type;
|
2202
|
+
};
|
2203
|
+
}
|
2204
|
+
|
2205
|
+
argspec.isCallback = function() {
|
2206
|
+
return function(obj) {
|
2207
|
+
return obj && obj.apply;
|
2208
|
+
};
|
2209
|
+
}
|
2210
|
+
}());
|
2211
|
+
|
2212
|
+
persistence.argspec = argspec;
|
2213
|
+
|
2214
|
+
return persistence;
|
2215
|
+
} // end of createPersistence
|
2216
|
+
|
2217
|
+
|
2218
|
+
|
2219
|
+
// JSON2 library, source: http://www.JSON.org/js.html
|
2220
|
+
// Most modern browsers already support this natively, but mobile
|
2221
|
+
// browsers often don't, hence this implementation
|
2222
|
+
// Relevant APIs:
|
2223
|
+
// JSON.stringify(value, replacer, space)
|
2224
|
+
// JSON.parse(text, reviver)
|
2225
|
+
|
2226
|
+
if(typeof JSON === 'undefined') {
|
2227
|
+
JSON = {};
|
2228
|
+
}
|
2229
|
+
//var JSON = typeof JSON === 'undefined' ? window.JSON : {};
|
2230
|
+
if (!JSON.stringify) {
|
2231
|
+
(function () {
|
2232
|
+
function f(n) {
|
2233
|
+
return n < 10 ? '0' + n : n;
|
2234
|
+
}
|
2235
|
+
if (typeof Date.prototype.toJSON !== 'function') {
|
2236
|
+
|
2237
|
+
Date.prototype.toJSON = function (key) {
|
2238
|
+
|
2239
|
+
return isFinite(this.valueOf()) ?
|
2240
|
+
this.getUTCFullYear() + '-' +
|
2241
|
+
f(this.getUTCMonth() + 1) + '-' +
|
2242
|
+
f(this.getUTCDate()) + 'T' +
|
2243
|
+
f(this.getUTCHours()) + ':' +
|
2244
|
+
f(this.getUTCMinutes()) + ':' +
|
2245
|
+
f(this.getUTCSeconds()) + 'Z' : null;
|
2246
|
+
};
|
2247
|
+
|
2248
|
+
String.prototype.toJSON =
|
2249
|
+
Number.prototype.toJSON =
|
2250
|
+
Boolean.prototype.toJSON = function (key) {
|
2251
|
+
return this.valueOf();
|
2252
|
+
};
|
2253
|
+
}
|
2254
|
+
|
2255
|
+
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
2256
|
+
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
2257
|
+
gap, indent,
|
2258
|
+
meta = {
|
2259
|
+
'\b': '\\b',
|
2260
|
+
'\t': '\\t',
|
2261
|
+
'\n': '\\n',
|
2262
|
+
'\f': '\\f',
|
2263
|
+
'\r': '\\r',
|
2264
|
+
'"' : '\\"',
|
2265
|
+
'\\': '\\\\'
|
2266
|
+
},
|
2267
|
+
rep;
|
2268
|
+
|
2269
|
+
function quote(string) {
|
2270
|
+
escapable.lastIndex = 0;
|
2271
|
+
return escapable.test(string) ?
|
2272
|
+
'"' + string.replace(escapable, function (a) {
|
2273
|
+
var c = meta[a];
|
2274
|
+
return typeof c === 'string' ? c :
|
2275
|
+
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
2276
|
+
}) + '"' :
|
2277
|
+
'"' + string + '"';
|
2278
|
+
}
|
2279
|
+
|
2280
|
+
|
2281
|
+
function str(key, holder) {
|
2282
|
+
var i, k, v, length, mind = gap, partial, value = holder[key];
|
2283
|
+
|
2284
|
+
if (value && typeof value === 'object' &&
|
2285
|
+
typeof value.toJSON === 'function') {
|
2286
|
+
value = value.toJSON(key);
|
2287
|
+
}
|
2288
|
+
|
2289
|
+
if (typeof rep === 'function') {
|
2290
|
+
value = rep.call(holder, key, value);
|
2291
|
+
}
|
2292
|
+
|
2293
|
+
switch (typeof value) {
|
2294
|
+
case 'string':
|
2295
|
+
return quote(value);
|
2296
|
+
case 'number':
|
2297
|
+
return isFinite(value) ? String(value) : 'null';
|
2298
|
+
case 'boolean':
|
2299
|
+
case 'null':
|
2300
|
+
return String(value);
|
2301
|
+
case 'object':
|
2302
|
+
if (!value) {
|
2303
|
+
return 'null';
|
2304
|
+
}
|
2305
|
+
|
2306
|
+
gap += indent;
|
2307
|
+
partial = [];
|
2308
|
+
|
2309
|
+
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
2310
|
+
length = value.length;
|
2311
|
+
for (i = 0; i < length; i += 1) {
|
2312
|
+
partial[i] = str(i, value) || 'null';
|
2313
|
+
}
|
2314
|
+
|
2315
|
+
v = partial.length === 0 ? '[]' :
|
2316
|
+
gap ? '[\n' + gap +
|
2317
|
+
partial.join(',\n' + gap) + '\n' +
|
2318
|
+
mind + ']' :
|
2319
|
+
'[' + partial.join(',') + ']';
|
2320
|
+
gap = mind;
|
2321
|
+
return v;
|
2322
|
+
}
|
2323
|
+
|
2324
|
+
if (rep && typeof rep === 'object') {
|
2325
|
+
length = rep.length;
|
2326
|
+
for (i = 0; i < length; i += 1) {
|
2327
|
+
k = rep[i];
|
2328
|
+
if (typeof k === 'string') {
|
2329
|
+
v = str(k, value);
|
2330
|
+
if (v) {
|
2331
|
+
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
2332
|
+
}
|
2333
|
+
}
|
2334
|
+
}
|
2335
|
+
} else {
|
2336
|
+
for (k in value) {
|
2337
|
+
if (Object.hasOwnProperty.call(value, k)) {
|
2338
|
+
v = str(k, value);
|
2339
|
+
if (v) {
|
2340
|
+
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
2341
|
+
}
|
2342
|
+
}
|
2343
|
+
}
|
2344
|
+
}
|
2345
|
+
|
2346
|
+
v = partial.length === 0 ? '{}' :
|
2347
|
+
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
|
2348
|
+
mind + '}' : '{' + partial.join(',') + '}';
|
2349
|
+
gap = mind;
|
2350
|
+
return v;
|
2351
|
+
}
|
2352
|
+
}
|
2353
|
+
|
2354
|
+
if (typeof JSON.stringify !== 'function') {
|
2355
|
+
JSON.stringify = function (value, replacer, space) {
|
2356
|
+
var i;
|
2357
|
+
gap = '';
|
2358
|
+
indent = '';
|
2359
|
+
if (typeof space === 'number') {
|
2360
|
+
for (i = 0; i < space; i += 1) {
|
2361
|
+
indent += ' ';
|
2362
|
+
}
|
2363
|
+
} else if (typeof space === 'string') {
|
2364
|
+
indent = space;
|
2365
|
+
}
|
2366
|
+
|
2367
|
+
rep = replacer;
|
2368
|
+
if (replacer && typeof replacer !== 'function' &&
|
2369
|
+
(typeof replacer !== 'object' ||
|
2370
|
+
typeof replacer.length !== 'number')) {
|
2371
|
+
throw new Error('JSON.stringify');
|
2372
|
+
}
|
2373
|
+
|
2374
|
+
return str('', {'': value});
|
2375
|
+
};
|
2376
|
+
}
|
2377
|
+
|
2378
|
+
if (typeof JSON.parse !== 'function') {
|
2379
|
+
JSON.parse = function (text, reviver) {
|
2380
|
+
var j;
|
2381
|
+
function walk(holder, key) {
|
2382
|
+
var k, v, value = holder[key];
|
2383
|
+
if (value && typeof value === 'object') {
|
2384
|
+
for (k in value) {
|
2385
|
+
if (Object.hasOwnProperty.call(value, k)) {
|
2386
|
+
v = walk(value, k);
|
2387
|
+
if (v !== undefined) {
|
2388
|
+
value[k] = v;
|
2389
|
+
} else {
|
2390
|
+
delete value[k];
|
2391
|
+
}
|
2392
|
+
}
|
2393
|
+
}
|
2394
|
+
}
|
2395
|
+
return reviver.call(holder, key, value);
|
2396
|
+
}
|
2397
|
+
|
2398
|
+
cx.lastIndex = 0;
|
2399
|
+
if (cx.test(text)) {
|
2400
|
+
text = text.replace(cx, function (a) {
|
2401
|
+
return '\\u' +
|
2402
|
+
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
2403
|
+
});
|
2404
|
+
}
|
2405
|
+
|
2406
|
+
if (/^[\],:{}\s]*$/.
|
2407
|
+
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
|
2408
|
+
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
|
2409
|
+
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
2410
|
+
j = eval('(' + text + ')');
|
2411
|
+
return typeof reviver === 'function' ?
|
2412
|
+
walk({'': j}, '') : j;
|
2413
|
+
}
|
2414
|
+
throw new SyntaxError('JSON.parse');
|
2415
|
+
};
|
2416
|
+
}
|
2417
|
+
}());
|
2418
|
+
}
|
2419
|
+
|