UNPKG

19.4 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);
206 }
207
208 if (typeof (callback) === 'function') {
209 callback();
210 }
211 });
212 };
213
214 // alias
215 LokiIndexedAdapter.prototype.deleteKey = LokiIndexedAdapter.prototype.deleteDatabase;
216
217 /**
218 * Removes all database partitions and pages with the base filename passed in.
219 * This utility method does not (yet) guarantee async deletions will be completed before returning
220 *
221 * @param {string} dbname - the base filename which container, partitions, or pages are derived
222 * @memberof LokiIndexedAdapter
223 */
224 LokiIndexedAdapter.prototype.deleteDatabasePartitions = function(dbname) {
225 var self=this;
226 this.getDatabaseList(function(result) {
227 result.forEach(function(str) {
228 if (str.startsWith(dbname)) {
229 self.deleteDatabase(str);
230 }
231 });
232 });
233 };
234
235 /**
236 * Retrieves object array of catalog entries for current app.
237 *
238 * @example
239 * idbAdapter.getDatabaseList(function(result) {
240 * // result is array of string names for that appcontext ('finance')
241 * result.forEach(function(str) {
242 * console.log(str);
243 * });
244 * });
245 *
246 * @param {function} callback - should accept array of database names in the catalog for current app.
247 * @memberof LokiIndexedAdapter
248 */
249 LokiIndexedAdapter.prototype.getDatabaseList = function(callback)
250 {
251 var appName = this.app;
252 var adapter = this;
253
254 // lazy open/create db reference so dont -need- callback in constructor
255 if (this.catalog === null || this.catalog.db === null) {
256 this.catalog = new LokiCatalog(function(cat) {
257 adapter.catalog = cat;
258
259 adapter.getDatabaseList(callback);
260 });
261
262 return;
263 }
264
265 // catalog already initialized
266 // get all keys for current appName, and transpose results so just string array
267 this.catalog.getAppKeys(appName, function(results) {
268 var names = [];
269
270 for(var idx = 0; idx < results.length; idx++) {
271 names.push(results[idx].key);
272 }
273
274 if (typeof (callback) === 'function') {
275 callback(names);
276 }
277 else {
278 names.forEach(function(obj) {
279 console.log(obj);
280 });
281 }
282 });
283 };
284
285 // alias
286 LokiIndexedAdapter.prototype.getKeyList = LokiIndexedAdapter.prototype.getDatabaseList;
287
288 /**
289 * Allows retrieval of list of all keys in catalog along with size
290 *
291 * @param {function} callback - (Optional) callback to accept result array.
292 * @memberof LokiIndexedAdapter
293 */
294 LokiIndexedAdapter.prototype.getCatalogSummary = function(callback)
295 {
296 var appName = this.app;
297 var adapter = this;
298
299 // lazy open/create db reference
300 if (this.catalog === null || this.catalog.db === null) {
301 this.catalog = new LokiCatalog(function(cat) {
302 adapter.catalog = cat;
303
304 adapter.getCatalogSummary(callback);
305 });
306
307 return;
308 }
309
310 // catalog already initialized
311 // get all keys for current appName, and transpose results so just string array
312 this.catalog.getAllKeys(function(results) {
313 var entries = [];
314 var obj,
315 size,
316 oapp,
317 okey,
318 oval;
319
320 for(var idx = 0; idx < results.length; idx++) {
321 obj = results[idx];
322 oapp = obj.app || '';
323 okey = obj.key || '';
324 oval = obj.val || '';
325
326 // app and key are composited into an appkey column so we will mult by 2
327 size = oapp.length * 2 + okey.length * 2 + oval.length + 1;
328
329 entries.push({ "app": obj.app, "key": obj.key, "size": size });
330 }
331
332 if (typeof (callback) === 'function') {
333 callback(entries);
334 }
335 else {
336 entries.forEach(function(obj) {
337 console.log(obj);
338 });
339 }
340 });
341 };
342
343 /**
344 * LokiCatalog - underlying App/Key/Value catalog persistence
345 * This non-interface class implements the actual persistence.
346 * Used by the IndexedAdapter class.
347 */
348 function LokiCatalog(callback)
349 {
350 this.db = null;
351 this.initializeLokiCatalog(callback);
352 }
353
354 LokiCatalog.prototype.initializeLokiCatalog = function(callback) {
355 var openRequest = indexedDB.open('LokiCatalog', 1);
356 var cat = this;
357
358 // If database doesn't exist yet or its version is lower than our version specified above (2nd param in line above)
359 openRequest.onupgradeneeded = function(e) {
360 var thisDB = e.target.result;
361 if (thisDB.objectStoreNames.contains('LokiAKV')) {
362 thisDB.deleteObjectStore('LokiAKV');
363 }
364
365 if(!thisDB.objectStoreNames.contains('LokiAKV')) {
366 var objectStore = thisDB.createObjectStore('LokiAKV', { keyPath: 'id', autoIncrement:true });
367 objectStore.createIndex('app', 'app', {unique:false});
368 objectStore.createIndex('key', 'key', {unique:false});
369 // hack to simulate composite key since overhead is low (main size should be in val field)
370 // user (me) required to duplicate the app and key into comma delimited appkey field off object
371 // This will allow retrieving single record with that composite key as well as
372 // still supporting opening cursors on app or key alone
373 objectStore.createIndex('appkey', 'appkey', {unique:true});
374 }
375 };
376
377 openRequest.onsuccess = function(e) {
378 cat.db = e.target.result;
379
380 if (typeof (callback) === 'function') callback(cat);
381 };
382
383 openRequest.onerror = function(e) {
384 throw e;
385 };
386 };
387
388 LokiCatalog.prototype.getAppKey = function(app, key, callback) {
389 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
390 var store = transaction.objectStore('LokiAKV');
391 var index = store.index('appkey');
392 var appkey = app + "," + key;
393 var request = index.get(appkey);
394
395 request.onsuccess = (function(usercallback) {
396 return function(e) {
397 var lres = e.target.result;
398
399 if (lres === null || typeof(lres) === 'undefined') {
400 lres = {
401 id: 0,
402 success: false
403 };
404 }
405
406 if (typeof(usercallback) === 'function') {
407 usercallback(lres);
408 }
409 else {
410 console.log(lres);
411 }
412 };
413 })(callback);
414
415 request.onerror = (function(usercallback) {
416 return function(e) {
417 if (typeof(usercallback) === 'function') {
418 usercallback({ id: 0, success: false });
419 }
420 else {
421 throw e;
422 }
423 };
424 })(callback);
425 };
426
427 LokiCatalog.prototype.getAppKeyById = function (id, callback, data) {
428 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
429 var store = transaction.objectStore('LokiAKV');
430 var request = store.get(id);
431
432 request.onsuccess = (function(data, usercallback){
433 return function(e) {
434 if (typeof(usercallback) === 'function') {
435 usercallback(e.target.result, data);
436 }
437 else {
438 console.log(e.target.result);
439 }
440 };
441 })(data, callback);
442 };
443
444 LokiCatalog.prototype.setAppKey = function (app, key, val, callback) {
445 var transaction = this.db.transaction(['LokiAKV'], 'readwrite');
446 var store = transaction.objectStore('LokiAKV');
447 var index = store.index('appkey');
448 var appkey = app + "," + key;
449 var request = index.get(appkey);
450
451 // first try to retrieve an existing object by that key
452 // 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
453 request.onsuccess = function(e) {
454 var res = e.target.result;
455
456 if (res === null || res === undefined) {
457 res = {
458 app:app,
459 key:key,
460 appkey: app + ',' + key,
461 val:val
462 };
463 }
464 else {
465 res.val = val;
466 }
467
468 var requestPut = store.put(res);
469
470 requestPut.onerror = (function(usercallback) {
471 return function(e) {
472 if (typeof(usercallback) === 'function') {
473 usercallback({ success: false });
474 }
475 else {
476 console.error('LokiCatalog.setAppKey (set) onerror');
477 console.error(request.error);
478 }
479 };
480
481 })(callback);
482
483 requestPut.onsuccess = (function(usercallback) {
484 return function(e) {
485 if (typeof(usercallback) === 'function') {
486 usercallback({ success: true });
487 }
488 };
489 })(callback);
490 };
491
492 request.onerror = (function(usercallback) {
493 return function(e) {
494 if (typeof(usercallback) === 'function') {
495 usercallback({ success: false });
496 }
497 else {
498 console.error('LokiCatalog.setAppKey (get) onerror');
499 console.error(request.error);
500 }
501 };
502 })(callback);
503 };
504
505 LokiCatalog.prototype.deleteAppKey = function (id, callback) {
506 var transaction = this.db.transaction(['LokiAKV'], 'readwrite');
507 var store = transaction.objectStore('LokiAKV');
508 var request = store.delete(id);
509
510 request.onsuccess = (function(usercallback) {
511 return function(evt) {
512 if (typeof(usercallback) === 'function') usercallback({ success: true });
513 };
514 })(callback);
515
516 request.onerror = (function(usercallback) {
517 return function(evt) {
518 if (typeof(usercallback) === 'function') {
519 usercallback(false);
520 }
521 else {
522 console.error('LokiCatalog.deleteAppKey raised onerror');
523 console.error(request.error);
524 }
525 };
526 })(callback);
527 };
528
529 LokiCatalog.prototype.getAppKeys = function(app, callback) {
530 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
531 var store = transaction.objectStore('LokiAKV');
532 var index = store.index('app');
533
534 // We want cursor to all values matching our (single) app param
535 var singleKeyRange = IDBKeyRange.only(app);
536
537 // To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor()
538 var cursor = index.openCursor(singleKeyRange);
539
540 // cursor internally, pushing results into this.data[] and return
541 // this.data[] when done (similar to service)
542 var localdata = [];
543
544 cursor.onsuccess = (function(data, callback) {
545 return function(e) {
546 var cursor = e.target.result;
547 if (cursor) {
548 var currObject = cursor.value;
549
550 data.push(currObject);
551
552 cursor.continue();
553 }
554 else {
555 if (typeof(callback) === 'function') {
556 callback(data);
557 }
558 else {
559 console.log(data);
560 }
561 }
562 };
563 })(localdata, callback);
564
565 cursor.onerror = (function(usercallback) {
566 return function(e) {
567 if (typeof(usercallback) === 'function') {
568 usercallback(null);
569 }
570 else {
571 console.error('LokiCatalog.getAppKeys raised onerror');
572 console.error(e);
573 }
574 };
575 })(callback);
576
577 };
578
579 // Hide 'cursoring' and return array of { id: id, key: key }
580 LokiCatalog.prototype.getAllKeys = function (callback) {
581 var transaction = this.db.transaction(['LokiAKV'], 'readonly');
582 var store = transaction.objectStore('LokiAKV');
583 var cursor = store.openCursor();
584
585 var localdata = [];
586
587 cursor.onsuccess = (function(data, callback) {
588 return function(e) {
589 var cursor = e.target.result;
590 if (cursor) {
591 var currObject = cursor.value;
592
593 data.push(currObject);
594
595 cursor.continue();
596 }
597 else {
598 if (typeof(callback) === 'function') {
599 callback(data);
600 }
601 else {
602 console.log(data);
603 }
604 }
605 };
606 })(localdata, callback);
607
608 cursor.onerror = (function(usercallback) {
609 return function(e) {
610 if (typeof(usercallback) === 'function') usercallback(null);
611 };
612 })(callback);
613
614 };
615
616 return LokiIndexedAdapter;
617
618 }());
619}));