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.
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
+