UNPKG

19.5 kBJavaScriptView Raw
1/*
2 Loki IndexedDb Adapter (need to include this script to use it)
3
4 Console Usage can be used for management/diagnostic, here are a few examples :
5 adapter.getDatabaseList(); // with no callback passed, this method will log results to console
6 adapter.saveDatabase('UserDatabase', JSON.stringify(myDb));
7 adapter.loadDatabase('UserDatabase'); // will log the serialized db to console
8 adapter.deleteDatabase('UserDatabase');
9*/
10
11(function (root, factory) {
12 if (typeof define === 'function' && define.amd) {
13 // AMD
14 define([], factory);
15 } else if (typeof exports === 'object') {
16 // Node, CommonJS-like
17 module.exports = factory();
18 } else {
19 // Browser globals (root is window)
20 root.LokiIndexedAdapter = factory();
21 }
22}(this, function () {
23 return (function() {
24
25 /**
26 * Loki persistence adapter class for indexedDb.
27 * This class fulfills abstract adapter interface which can be applied to other storage methods.
28 * Utilizes the included LokiCatalog app/key/value database for actual database persistence.
29 * Indexeddb is highly async, but this adapter has been made 'console-friendly' as well.
30 * Anywhere a callback is omitted, it should return results (if applicable) to console.
31 * IndexedDb storage is provided per-domain, so we implement app/key/value database to
32 * allow separate contexts for separate apps within a domain.
33 *
34 * @example
35 * var idbAdapter = new LokiIndexedAdapter('finance');
36 *
37 * @constructor LokiIndexedAdapter
38 *
39 * @param {string} appname - (Optional) Application name context can be used to distinguish subdomains, 'loki' by default
40 */
41 function LokiIndexedAdapter(appname)
42 {
43 this.app = 'loki';
44
45 if (typeof (appname) !== 'undefined')
46 {
47 this.app = appname;
48 }
49
50 // keep reference to catalog class for base AKV operations
51 this.catalog = null;
52
53 if (!this.checkAvailability()) {
54 throw new Error('indexedDB does not seem to be supported for your environment');
55 }
56 }
57
58 /**
59 * Used to check if adapter is available
60 *
61 * @returns {boolean} true if indexeddb is available, false if not.
62 * @memberof LokiIndexedAdapter
63 */
64 LokiIndexedAdapter.prototype.checkAvailability = function()
65 {
66 if (typeof indexedDB !== 'undefined' && indexedDB) return true;
67
68 return false;
69 };
70
71 /**
72 * Retrieves a serialized db string from the catalog.
73 *
74 * @example
75 * // LOAD
76 * var idbAdapter = new LokiIndexedAdapter('finance');
77 * var db = new loki('test', { adapter: idbAdapter });
78 * db.loadDatabase(function(result) {
79 * console.log('done');
80 * });
81 *
82 * @param {string} dbname - the name of the database to retrieve.
83 * @param {function} callback - callback should accept string param containing serialized db string.
84 * @memberof LokiIndexedAdapter
85 */
86 LokiIndexedAdapter.prototype.loadDatabase = function(dbname, callback)
87 {
88 var appName = this.app;
89 var adapter = this;
90
91 // lazy open/create db reference so dont -need- callback in constructor
92 if (this.catalog === null || this.catalog.db === null) {
93 this.catalog = new LokiCatalog(function(cat) {
94 adapter.catalog = cat;
95
96 adapter.loadDatabase(dbname, callback);
97 });
98
99 return;
100 }
101
102 // lookup up db string in AKV db
103 this.catalog.getAppKey(appName, dbname, function(result) {
104 if (typeof (callback) === 'function') {
105 if (result.id === 0) {
106 callback(null);
107 return;
108 }
109 callback(result.val);
110 }
111 else {
112 // support console use of api
113 console.log(result.val);
114 }
115 });
116 };
117
118 // alias
119 LokiIndexedAdapter.prototype.loadKey = LokiIndexedAdapter.prototype.loadDatabase;
120
121 /**
122 * Saves a serialized db to the catalog.
123 *
124 * @example
125 * // SAVE : will save App/Key/Val as 'finance'/'test'/{serializedDb}
126 * var idbAdapter = new LokiIndexedAdapter('finance');
127 * var db = new loki('test', { adapter: idbAdapter });
128 * var coll = db.addCollection('testColl');
129 * coll.insert({test: 'val'});
130 * db.saveDatabase(); // could pass callback if needed for async complete
131 *
132 * @param {string} dbname - the name to give the serialized database within the catalog.
133 * @param {string} dbstring - the serialized db string to save.
134 * @param {function} callback - (Optional) callback passed obj.success with true or false
135 * @memberof LokiIndexedAdapter
136 */
137 LokiIndexedAdapter.prototype.saveDatabase = function(dbname, dbstring, callback)
138 {
139 var appName = this.app;
140 var adapter = this;
141
142 function saveCallback(result) {
143 if (result && result.success === true) {
144 callback(null);
145 }
146 else {
147 callback(new Error("Error saving database"));
148 }
149 }
150
151 // lazy open/create db reference so dont -need- callback in constructor
152 if (this.catalog === null || this.catalog.db === null) {
153 this.catalog = new LokiCatalog(function(cat) {
154 adapter.catalog = cat;
155
156 // now that catalog has been initialized, set (add/update) the AKV entry
157 cat.setAppKey(appName, dbname, dbstring, saveCallback);
158 });
159
160 return;
161 }
162
163 // set (add/update) entry to AKV database
164 this.catalog.setAppKey(appName, dbname, dbstring, saveCallback);
165 };
166
167 // alias
168 LokiIndexedAdapter.prototype.saveKey = LokiIndexedAdapter.prototype.saveDatabase;
169
170 /**
171 * Deletes a serialized db from the catalog.
172 *
173 * @example
174 * // DELETE DATABASE
175 * // delete 'finance'/'test' value from catalog
176 * idbAdapter.deleteDatabase('test', function {
177 * // database deleted
178 * });
179 *
180 * @param {string} dbname - the name of the database to delete from the catalog.
181 * @param {function=} callback - (Optional) executed on database delete
182 * @memberof LokiIndexedAdapter
183 */
184 LokiIndexedAdapter.prototype.deleteDatabase = function(dbname, callback)
185 {
186 var appName = this.app;
187 var adapter = this;
188
189 // lazy open/create db reference and pass callback ahead
190 if (this.catalog === null || this.catalog.db === null) {
191 this.catalog = new LokiCatalog(function(cat) {
192 adapter.catalog = cat;
193
194 adapter.deleteDatabase(dbname, callback);
195 });
196
197 return;
198 }
199
200 // catalog was already initialized, so just lookup object and delete by id
201 this.catalog.getAppKey(appName, dbname, function(result) {
202 var id = result.id;
203
204 if (id !== 0) {
205 adapter.catalog.deleteAppKey(id, callback);
206 } else if (typeof (callback) === 'function') {
207 callback({ success: true });
208 }
209 });
210 };
211
212 // alias
213 LokiIndexedAdapter.prototype.deleteKey = LokiIndexedAdapter.prototype.deleteDatabase;
214
215 /**
216 * Removes all database partitions and pages with the base filename passed in.
217 * This utility method does not (yet) guarantee async deletions will be completed before returning
218 *
219 * @param {string} dbname - the base filename which container, partitions, or pages are derived
220 * @memberof LokiIndexedAdapter
221 */
222 LokiIndexedAdapter.prototype.deleteDatabasePartitions = function(dbname) {
223 var self=this;
224 this.getDatabaseList(function(result) {
225 result.forEach(function(str) {
226 if (str.startsWith(dbname)) {
227 self.deleteDatabase(str);
228 }
229 });
230 });
231 };
232
233 /**
234 * Retrieves object array of catalog entries for current app.
235 *
236 * @example
237 * idbAdapter.getDatabaseList(function(result) {
238 * // result is array of string names for that appcontext ('finance')
239 * result.forEach(function(str) {
240 * console.log(str);
241 * });
242 * });
243 *
244 * @param {function} callback - should accept array of database names in the catalog for current app.
245 * @memberof LokiIndexedAdapter
246 */
247 LokiIndexedAdapter.prototype.getDatabaseList = function(callback)
248 {
249 var appName = this.app;
250 var adapter = this;
251
252 // lazy open/create db reference so dont -need- callback in constructor
253 if (this.catalog === null || this.catalog.db === null) {
254 this.catalog = new LokiCatalog(function(cat) {
255 adapter.catalog = cat;
256
257 adapter.getDatabaseList(callback);
258 });
259
260 return;
261 }
262
263 // catalog already initialized
264 // get all keys for current appName, and transpose results so just string array
265 this.catalog.getAppKeys(appName, function(results) {
266 var names = [];
267
268 for(var idx = 0; idx < results.length; idx++) {
269 names.push(results[idx].key);
270 }
271
272 if (typeof (callback) === 'function') {
273 callback(names);
274 }
275 else {
276 names.forEach(function(obj) {
277 console.log(obj);
278 });
279 }
280 });
281 };
282
283 // alias
284 LokiIndexedAdapter.prototype.getKeyList = LokiIndexedAdapter.prototype.getDatabaseList;
285
286 /**
287 * Allows retrieval of list of all keys in catalog along with size
288 *
289 * @param {function} callback - (Optional) callback to accept result array.
290 * @memberof LokiIndexedAdapter
291 */
292 LokiIndexedAdapter.prototype.getCatalogSummary = function(callback)
293 {
294 var appName = this.app;
295 var adapter = this;
296
297 // lazy open/create db reference
298 if (this.catalog === null || this.catalog.db === null) {
299 this.catalog = new LokiCatalog(function(cat) {
300 adapter.catalog = cat;
301
302 adapter.getCatalogSummary(callback);
303 });
304
305 return;
306 }
307
308 // catalog already initialized
309 // get all keys for current appName, and transpose results so just string array
310 this.catalog.getAllKeys(function(results) {
311 var entries = [];
312 var obj,
313 size,
314 oapp,
315 okey,
316 oval;
317
318 for(var idx = 0; idx < results.length; idx++) {
319 obj = results[idx];
320 oapp = obj.app || '';
321 okey = obj.key || '';
322 oval = obj.val || '';
323
324 // app and key are composited into an appkey column so we will mult by 2
325 size = oapp.length * 2 + okey.length * 2 + oval.length + 1;
326
327 entries.push({ "app": obj.app, "key": obj.key, "size": size });
328 }
329
330 if (typeof (callback) === 'function') {
331 callback(entries);
332 }
333 else {
334 entries.forEach(function(obj) {
335 console.log(obj);
336 });
337 }
338 });
339 };
340
341 /**
342 * LokiCatalog - underlying App/Key/Value catalog persistence
343 * This non-interface class implements the actual persistence.
344 * Used by the IndexedAdapter class.
345 */
346 function LokiCatalog(callback)
347 {
348 this.db = null;
349 this.initializeLokiCatalog(callback);
350 }
351
352 LokiCatalog.prototype.initializeLokiCatalog = function(callback) {
353 var openRequest = indexedDB.open('LokiCatalog', 1);
354 var cat = this;
355
356 // If database doesn't exist yet or its version is lower than our version specified above (2nd param in line above)
357 openRequest.onupgradeneeded = function(e) {
358 var thisDB = e.target.result;
359 if (thisDB.objectStoreNames.contains('LokiAKV')) {
360 thisDB.deleteObjectStore('LokiAKV');
361 }
362
363 if(!thisDB.objectStoreNames.contains('LokiAKV')) {
364 var objectStore = thisDB.createObjectStore('LokiAKV', { keyPath: 'id', autoIncrement:true });
365 objectStore.createIndex('app', 'app', {unique:false});
366 objectStore.createIndex('key', 'key', {unique:false});
367 // hack to simulate composite key since overhead is low (main size should be in val field)
368 // user (me) required to duplicate the app and key into comma delimited appkey field off object
369 // This will allow retrieving single record with that composite key as well as
370 // still supporting opening cursors on app or key alone
371 objectStore.createIndex('appkey', 'appkey', {unique:true});
372 }
373 };
374
375 openRequest.onsuccess = function(e) {
376 cat.db = e.target.result;
377
378 if (typeof (callback) === 'function') callback(cat);
379 };
380
381 openRequest.onerror = function(e) {
382 throw e;
383 };
384 };
385
386 LokiCatalog.prototype.getAppKey = function(app, key, callback) {
387 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
388 var store = transaction.objectStore('LokiAKV');
389 var index = store.index('appkey');
390 var appkey = app + "," + key;
391 var request = index.get(appkey);
392
393 request.onsuccess = (function(usercallback) {
394 return function(e) {
395 var lres = e.target.result;
396
397 if (lres === null || typeof(lres) === 'undefined') {
398 lres = {
399 id: 0,
400 success: false
401 };
402 }
403
404 if (typeof(usercallback) === 'function') {
405 usercallback(lres);
406 }
407 else {
408 console.log(lres);
409 }
410 };
411 })(callback);
412
413 request.onerror = (function(usercallback) {
414 return function(e) {
415 if (typeof(usercallback) === 'function') {
416 usercallback({ id: 0, success: false });
417 }
418 else {
419 throw e;
420 }
421 };
422 })(callback);
423 };
424
425 LokiCatalog.prototype.getAppKeyById = function (id, callback, data) {
426 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
427 var store = transaction.objectStore('LokiAKV');
428 var request = store.get(id);
429
430 request.onsuccess = (function(data, usercallback){
431 return function(e) {
432 if (typeof(usercallback) === 'function') {
433 usercallback(e.target.result, data);
434 }
435 else {
436 console.log(e.target.result);
437 }
438 };
439 })(data, callback);
440 };
441
442 LokiCatalog.prototype.setAppKey = function (app, key, val, callback) {
443 var transaction = this.db.transaction(['LokiAKV'], 'readwrite');
444 var store = transaction.objectStore('LokiAKV');
445 var index = store.index('appkey');
446 var appkey = app + "," + key;
447 var request = index.get(appkey);
448
449 // first try to retrieve an existing object by that key
450 // need to do this because to update an object you need to have id in object, otherwise it will append id with new autocounter and clash the unique index appkey
451 request.onsuccess = function(e) {
452 var res = e.target.result;
453
454 if (res === null || res === undefined) {
455 res = {
456 app:app,
457 key:key,
458 appkey: app + ',' + key,
459 val:val
460 };
461 }
462 else {
463 res.val = val;
464 }
465
466 var requestPut = store.put(res);
467
468 requestPut.onerror = (function(usercallback) {
469 return function(e) {
470 if (typeof(usercallback) === 'function') {
471 usercallback({ success: false });
472 }
473 else {
474 console.error('LokiCatalog.setAppKey (set) onerror');
475 console.error(request.error);
476 }
477 };
478
479 })(callback);
480
481 requestPut.onsuccess = (function(usercallback) {
482 return function(e) {
483 if (typeof(usercallback) === 'function') {
484 usercallback({ success: true });
485 }
486 };
487 })(callback);
488 };
489
490 request.onerror = (function(usercallback) {
491 return function(e) {
492 if (typeof(usercallback) === 'function') {
493 usercallback({ success: false });
494 }
495 else {
496 console.error('LokiCatalog.setAppKey (get) onerror');
497 console.error(request.error);
498 }
499 };
500 })(callback);
501 };
502
503 LokiCatalog.prototype.deleteAppKey = function (id, callback) {
504 var transaction = this.db.transaction(['LokiAKV'], 'readwrite');
505 var store = transaction.objectStore('LokiAKV');
506 var request = store.delete(id);
507
508 request.onsuccess = (function(usercallback) {
509 return function(evt) {
510 if (typeof(usercallback) === 'function') usercallback({ success: true });
511 };
512 })(callback);
513
514 request.onerror = (function(usercallback) {
515 return function(evt) {
516 if (typeof(usercallback) === 'function') {
517 usercallback({ success: false });
518 }
519 else {
520 console.error('LokiCatalog.deleteAppKey raised onerror');
521 console.error(request.error);
522 }
523 };
524 })(callback);
525 };
526
527 LokiCatalog.prototype.getAppKeys = function(app, callback) {
528 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
529 var store = transaction.objectStore('LokiAKV');
530 var index = store.index('app');
531
532 // We want cursor to all values matching our (single) app param
533 var singleKeyRange = IDBKeyRange.only(app);
534
535 // To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor()
536 var cursor = index.openCursor(singleKeyRange);
537
538 // cursor internally, pushing results into this.data[] and return
539 // this.data[] when done (similar to service)
540 var localdata = [];
541
542 cursor.onsuccess = (function(data, callback) {
543 return function(e) {
544 var cursor = e.target.result;
545 if (cursor) {
546 var currObject = cursor.value;
547
548 data.push(currObject);
549
550 cursor.continue();
551 }
552 else {
553 if (typeof(callback) === 'function') {
554 callback(data);
555 }
556 else {
557 console.log(data);
558 }
559 }
560 };
561 })(localdata, callback);
562
563 cursor.onerror = (function(usercallback) {
564 return function(e) {
565 if (typeof(usercallback) === 'function') {
566 usercallback(null);
567 }
568 else {
569 console.error('LokiCatalog.getAppKeys raised onerror');
570 console.error(e);
571 }
572 };
573 })(callback);
574
575 };
576
577 // Hide 'cursoring' and return array of { id: id, key: key }
578 LokiCatalog.prototype.getAllKeys = function (callback) {
579 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
580 var store = transaction.objectStore('LokiAKV');
581 var cursor = store.openCursor();
582
583 var localdata = [];
584
585 cursor.onsuccess = (function(data, callback) {
586 return function(e) {
587 var cursor = e.target.result;
588 if (cursor) {
589 var currObject = cursor.value;
590
591 data.push(currObject);
592
593 cursor.continue();
594 }
595 else {
596 if (typeof(callback) === 'function') {
597 callback(data);
598 }
599 else {
600 console.log(data);
601 }
602 }
603 };
604 })(localdata, callback);
605
606 cursor.onerror = (function(usercallback) {
607 return function(e) {
608 if (typeof(usercallback) === 'function') usercallback(null);
609 };
610 })(callback);
611
612 };
613
614 return LokiIndexedAdapter;
615
616 }());
617}));