UNPKG

lokijs

Version:

Fast document oriented javascript in-memory database

570 lines (476 loc) 17.1 kB
/* Loki IndexedDb Adapter (need to include this script to use it) Indexeddb is highly async, but this adapter has been made 'console-friendly' as well. Anywhere a callback is omitted, it should return results (if applicable) to console. IndexedDb storage is provided per-domain, so we implement app/key/value database to allow separate contexts for separate apps within a domain. */ /* Examples : // SAVE : will save App/Key/Val as 'finance'/'test'/{serializedDb} // if appContect ('finance' in this example) is omitted, 'loki' will be used var idbAdapter = new lokiIndexedAdapter('finance'); var db = new loki('test', { adapter: idbAdapter }); var coll = db.addCollection('testColl'); coll.insert({test: 'val'}); db.saveDatabase(); // could pass callback if needed for async complete // LOAD var idbAdapter = new lokiIndexedAdapter('finance'); var db = new loki('test', { adapter: idbAdapter }); db.loadDatabase(function(result) { console.log('done'); }); // GET DATABASE LIST var idbAdapter = new lokiIndexedAdapter('finance'); idbAdapter.getDatabaseList(function(result) { // result is array of string names for that appcontext ('finance') result.forEach(function(str) { console.log(str); }); }); // DELETE DATABASE var idbAdapter = new lokiIndexedAdapter('finance'); idbAdapter.deleteDatabase('test'); // delete 'finance'/'test' value from catalog // CONSOLE USAGE : if using from console for management/diagnostic, here are a few examples : var adapter = new lokiIndexedAdapter('loki'); // or whatever appContext you want to use adapter.getDatabaseList(); // with no callback passed, this method will log results to console adapter.saveDatabase('UserDatabase', JSON.stringify(myDb)); adapter.loadDatabase('UserDatabase'); // will log the serialized db to console adapter.deleteDatabase('UserDatabase'); */ // global adapter - returned at end as IndexedAdapter constructor var lokiIndexedAdapter = (function() { /** * IndexedAdapter - Loki persistence adapter class for indexedDb. * This class fulfills abstract adapter interface which can be applied to other storage methods * Utilizes the included LokiCatalog app/key/value database for actual database persistence. * * @param {string} appname - Application name context can be used to distinguish subdomains or just 'loki' */ function IndexedAdapter(appname) { this.app = 'loki'; if (typeof (appname) !== 'undefined') { this.app = appname; } // keep reference to catalog class for base AKV operations this.catalog = null; if (!this.checkAvailability()) { console.error('indexedDB does not seem to be supported for your environment'); } } /** * checkAvailability - used to check if adapter is available * * @returns {boolean} true if indexeddb is available, false if not. */ IndexedAdapter.prototype.checkAvailability = function() { if (window && window.indexedDB) return true; return false; } /** * loadDatabase() - Retrieves a serialized db string from the catalog. * * @param {string} dbname - the name of the database to retrieve. * @param {function} callback - callback should accept string param containing serialized db string. */ IndexedAdapter.prototype.loadDatabase = function(dbname, callback) { var appName = this.app; var adapter = this; // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.loadDatabase(dbname, callback); }); return; } // lookup up db string in AKV db this.catalog.getAppKey(appName, dbname, function(result) { if (typeof (callback) === 'function') { if (result.id === 0) { console.warn("loki indexeddb adapter could not find database"); callback(null); return; } callback(result.val); } else { // support console use of api console.log(result.val); } }); } // alias IndexedAdapter.prototype.loadKey = IndexedAdapter.prototype.loadDatabase; /** * saveDatabase() - Saves a serialized db to the catalog. * * @param {string} dbname - the name to give the serialized database within the catalog. * @param {string} dbstring - the serialized db string to save. * @param {function} callback - (Optional) callback passed obj.success with true or false */ IndexedAdapter.prototype.saveDatabase = function(dbname, dbstring, callback) { var appName = this.app; var adapter = this; // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; // now that catalog has been initialized, set (add/update) the AKV entry cat.setAppKey(appName, dbname, dbstring, callback); }); return; } // set (add/update) entry to AKV database this.catalog.setAppKey(appName, dbname, dbstring, callback); } // alias IndexedAdapter.prototype.saveKey = IndexedAdapter.prototype.saveDatabase; /** * deleteDatabase() - Deletes a serialized db from the catalog. * * @param {string} dbname - the name of the database to delete from the catalog. */ IndexedAdapter.prototype.deleteDatabase = function(dbname) { var appName = this.app; var adapter = this; // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.deleteDatabase(dbname); }); return; } // catalog was already initialized, so just lookup object and delete by id this.catalog.getAppKey(appName, dbname, function(result) { var id = result.id; if (id !== 0) { adapter.catalog.deleteAppKey(id); } }); } // alias IndexedAdapter.prototype.deleteKey = IndexedAdapter.prototype.deleteDatabase; /** * getDatabaseList() - Retrieves object array of catalog entries for current app. * * @param {function} callback - should accept array of database names in the catalog for current app. */ IndexedAdapter.prototype.getDatabaseList = function(callback) { var appName = this.app; var adapter = this; // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.getDatabaseList(callback); }); return; } // catalog already initialized // get all keys for current appName, and transpose results so just string array this.catalog.getAppKeys(appName, function(results) { var names = []; for(var idx = 0; idx < results.length; idx++) { names.push(results[idx].key); } if (typeof (callback) === 'function') { callback(names); } else { names.forEach(function(obj) { console.log(obj); }); } }); } // alias IndexedAdapter.prototype.getKeyList = IndexedAdapter.prototype.getDatabaseList; /** * getCatalogSummary - allows retrieval of list of all keys in catalog along with size * * @param {function} callback - (Optional) callback to accept result array. */ IndexedAdapter.prototype.getCatalogSummary = function(callback) { var appName = this.app; var adapter = this; // lazy open/create db reference if (this.catalog === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.getCatalogSummary(callback); }); return; } // catalog already initialized // get all keys for current appName, and transpose results so just string array this.catalog.getAllKeys(function(results) { var entries = []; var obj, size, oapp, okey, oval; for(var idx = 0; idx < results.length; idx++) { obj = results[idx]; oapp = obj.app || ''; okey = obj.key || ''; oval = obj.val || ''; // app and key are composited into an appkey column so we will mult by 2 size = oapp.length * 2 + okey.length * 2 + oval.length + 1; entries.push({ "app": obj.app, "key": obj.key, "size": size }); } if (typeof (callback) === 'function') { callback(entries); } else { entries.forEach(function(obj) { console.log(obj); }); } }); } /** * LokiCatalog - underlying App/Key/Value catalog persistence * This non-interface class implements the actual persistence. * Used by the IndexedAdapter class. */ function LokiCatalog(callback) { this.db = null; this.initializeLokiCatalog(callback); } LokiCatalog.prototype.initializeLokiCatalog = function(callback) { var openRequest = indexedDB.open('LokiCatalog', 1); var cat = this; // If database doesn't exist yet or its version is lower than our version specified above (2nd param in line above) openRequest.onupgradeneeded = function(e) { var thisDB = e.target.result; if (thisDB.objectStoreNames.contains('LokiAKV')) { thisDB.deleteObjectStore('LokiAKV'); } if(!thisDB.objectStoreNames.contains('LokiAKV')) { var objectStore = thisDB.createObjectStore('LokiAKV', { keyPath: 'id', autoIncrement:true }); objectStore.createIndex('app', 'app', {unique:false}); objectStore.createIndex('key', 'key', {unique:false}); // hack to simulate composite key since overhead is low (main size should be in val field) // user (me) required to duplicate the app and key into comma delimited appkey field off object // This will allow retrieving single record with that composite key as well as // still supporting opening cursors on app or key alone objectStore.createIndex('appkey', 'appkey', {unique:true}); } } openRequest.onsuccess = function(e) { cat.db = e.target.result; if (typeof (callback) === 'function') callback(cat); } openRequest.onerror = function(e) { throw e; } } LokiCatalog.prototype.getAppKey = function(app, key, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var index = store.index('appkey'); var appkey = app + "," + key; var request = index.get(appkey); request.onsuccess = (function(usercallback) { return function(e) { var lres = e.target.result; if (typeof(lres) === 'undefined') { lres = { id: 0, success: false }; } if (typeof(usercallback) === 'function') { usercallback(lres); } else { console.log(lres); } } })(callback); request.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ id: 0, success: false }); } else { throw e; } } })(callback); } LokiCatalog.prototype.getAppKeyById = function (id, callback, data) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var request = store.get(id); request.onsuccess = (function(data, usercallback){ return function(e) { if (typeof(usercallback) === 'function') { usercallback(e.target.result, data); } else { console.log(e.target.result); } }; })(data, callback); } LokiCatalog.prototype.setAppKey = function (app, key, val, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readwrite'); var store = transaction.objectStore('LokiAKV'); var index = store.index('appkey'); var appkey = app + "," + key; var request = index.get(appkey); // first try to retrieve an existing object by that key // 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 request.onsuccess = function(e) { var res = e.target.result; if (res == null) { res = { app:app, key:key, appkey: app + ',' + key, val:val } } else { res.val = val; } var requestPut = store.put(res); requestPut.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ success: false }); } else { console.error('LokiCatalog.setAppKey (set) onerror'); console.error(request.error); } } })(callback); requestPut.onsuccess = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ success: true }); } } })(callback); }; request.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ success: false }); } else { console.error('LokiCatalog.setAppKey (get) onerror'); console.error(request.error); } } })(callback); } LokiCatalog.prototype.deleteAppKey = function (id, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readwrite'); var store = transaction.objectStore('LokiAKV'); var request = store.delete(id); request.onsuccess = (function(usercallback) { return function(evt) { if (typeof(usercallback) === 'function') usercallback({ success: true }); }; })(callback); request.onerror = (function(usercallback) { return function(evt) { if (typeof(usercallback) === 'function') { usercallback(false); } else { console.error('LokiCatalog.deleteAppKey raised onerror'); console.error(request.error); } } })(callback); } LokiCatalog.prototype.getAppKeys = function(app, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var index = store.index('app'); // We want cursor to all values matching our (single) app param var singleKeyRange = IDBKeyRange.only(app); // To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor() var cursor = index.openCursor(singleKeyRange); // cursor internally, pushing results into this.data[] and return // this.data[] when done (similar to service) var localdata = []; cursor.onsuccess = (function(data, callback) { return function(e) { var cursor = e.target.result; if (cursor) { var currObject = cursor.value; data.push(currObject); cursor.continue(); } else { if (typeof(callback) === 'function') { callback(data); } else { console.log(data); } } } })(localdata, callback); cursor.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback(null); } else { console.error('LokiCatalog.getAppKeys raised onerror'); console.error(e); } } })(callback); } // Hide 'cursoring' and return array of { id: id, key: key } LokiCatalog.prototype.getAllKeys = function (callback) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var cursor = store.openCursor(); var localdata = []; cursor.onsuccess = (function(data, callback) { return function(e) { var cursor = e.target.result; if (cursor) { var currObject = cursor.value; data.push(currObject); cursor.continue(); } else { if (typeof(callback) === 'function') { callback(data); } else { console.log(data); } } } })(localdata, callback); cursor.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') usercallback(null); } })(callback); } return IndexedAdapter; }());