persistence-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/README.md +36 -0
  4. data/Rakefile +2 -0
  5. data/lib/generators/persistence/install_generator.rb +22 -0
  6. data/lib/generators/persistence/templates/application.js +10 -0
  7. data/lib/persistence-rails.rb +1 -0
  8. data/lib/persistence/rails.rb +8 -0
  9. data/lib/persistence/rails/engine.rb +6 -0
  10. data/lib/persistence/rails/version.rb +5 -0
  11. data/persistence-rails.gemspec +17 -0
  12. data/vendor/assets/javascript/persistence.all.js +16 -0
  13. data/vendor/assets/javascript/persistence.core.js +2419 -0
  14. data/vendor/assets/javascript/persistence.jquery.js +103 -0
  15. data/vendor/assets/javascript/persistence.jquery.mobile.js +256 -0
  16. data/vendor/assets/javascript/persistence.js +5 -0
  17. data/vendor/assets/javascript/persistence.migrations.js +303 -0
  18. data/vendor/assets/javascript/persistence.pool.js +47 -0
  19. data/vendor/assets/javascript/persistence.search.js +293 -0
  20. data/vendor/assets/javascript/persistence.store.appengine.js +412 -0
  21. data/vendor/assets/javascript/persistence.store.config.js +29 -0
  22. data/vendor/assets/javascript/persistence.store.memory.js +239 -0
  23. data/vendor/assets/javascript/persistence.store.mysql.js +127 -0
  24. data/vendor/assets/javascript/persistence.store.sql.js +900 -0
  25. data/vendor/assets/javascript/persistence.store.sqlite.js +123 -0
  26. data/vendor/assets/javascript/persistence.store.sqlite3.js +124 -0
  27. data/vendor/assets/javascript/persistence.store.titanium.js +193 -0
  28. data/vendor/assets/javascript/persistence.store.websql.js +218 -0
  29. data/vendor/assets/javascript/persistence.sync.js +353 -0
  30. metadata +76 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in persistence-rails.gemspec
4
+ gemspec
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,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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,8 @@
1
+ require "persistence/rails/engine"
2
+ require "persistence/rails/version"
3
+
4
+ # module Persistence
5
+ # module Rails
6
+ # # Your code goes here...
7
+ # end
8
+ # end
@@ -0,0 +1,6 @@
1
+ module Persistence
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Persistence
2
+ module Rails
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -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
+