From 4b3d7a9a8053aa1ea5ec5ce99afe4f1880d16da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Hildinger?= Date: Sat, 26 Mar 2016 13:25:26 -0300 Subject: [PATCH 1/3] Adding return to iterate.onItem to increment "recordCount" only if it returns "undefined" (keep compatibility) or "true". Because IndexedDB is limited on filtering, I had to use iterate and manually check each item with custom filters. But I still need to limit the quantity of records returned, so I don't iterate the whole store, I know that there is the param "limit" the problem is that IDBWrapper doesn't know how many records passed my filter. To solve this, I changed the "onItem" function to return a boolean and increment "recordCount" only if it returns "undefined" (keep compatibility) or "true". --- idbstore.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/idbstore.js b/idbstore.js index 0e1e36e..5da8401 100644 --- a/idbstore.js +++ b/idbstore.js @@ -1198,8 +1198,10 @@ cursor.advance(options.offset); options.offset = 0; } else { - onItem(cursor.value, cursor, cursorTransaction); - recordCount++; + var onItemReturn = onItem(cursor.value, cursor, cursorTransaction); + if (onItemReturn === undefined || onItemReturn) { + recordCount++; + } if (options.autoContinue) { if (recordCount + options.offset < options.limit) { cursor['continue'](); From 916fe9c0aaae765573c5f9c4bf8147ffbc29286a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Hildinger?= Date: Sun, 27 Mar 2016 19:57:13 -0300 Subject: [PATCH 2/3] Refactor to receive the param "dbName" instead of "storePrefix" and an array of stores. Creation of a class called "Store" which contains the specific attributes of a store. Main class changed to receive the param dbName and an array of store params to create each store inside the DB. With these changes the user is able to create one DB with multiple stores. Tests changed to comply with the new behavior. --- idbstore.js | 2575 ++++++++++++++++++++------------------- test/idbwrapper_spec.js | 168 +-- 2 files changed, 1401 insertions(+), 1342 deletions(-) diff --git a/idbstore.js b/idbstore.js index 5da8401..84b4582 100644 --- a/idbstore.js +++ b/idbstore.js @@ -9,1365 +9,1406 @@ * Licensed under the MIT (X11) license */ -(function (name, definition, global) { - - 'use strict'; - - if (typeof define === 'function') { - define(definition); - } else if (typeof module !== 'undefined' && module.exports) { - module.exports = definition(); - } else { - global[name] = definition(); - } -})('IDBStore', function () { - - 'use strict'; - - var defaultErrorHandler = function (error) { - throw error; - }; - var defaultSuccessHandler = function () {}; - - var defaults = { - storeName: 'Store', - storePrefix: 'IDBWrapper-', - dbVersion: 1, - keyPath: 'id', - autoIncrement: true, - onStoreReady: function () { - }, - onError: defaultErrorHandler, - indexes: [], - implementationPreference: [ - 'indexedDB', - 'webkitIndexedDB', - 'mozIndexedDB', - 'shimIndexedDB' - ] - }; - - /** - * - * The IDBStore constructor - * - * @constructor - * @name IDBStore - * @version 1.6.2 - * - * @param {Object} [kwArgs] An options object used to configure the store and - * set callbacks - * @param {String} [kwArgs.storeName='Store'] The name of the store - * @param {String} [kwArgs.storePrefix='IDBWrapper-'] A prefix that is - * internally used to construct the name of the database, which will be - * kwArgs.storePrefix + kwArgs.storeName - * @param {Number} [kwArgs.dbVersion=1] The version of the store - * @param {String} [kwArgs.keyPath='id'] The key path to use. If you want to - * setup IDBWrapper to work with out-of-line keys, you need to set this to - * `null` - * @param {Boolean} [kwArgs.autoIncrement=true] If set to true, IDBStore will - * automatically make sure a unique keyPath value is present on each object - * that is stored. - * @param {Function} [kwArgs.onStoreReady] A callback to be called when the - * store is ready to be used. - * @param {Function} [kwArgs.onError=throw] A callback to be called when an - * error occurred during instantiation of the store. - * @param {Array} [kwArgs.indexes=[]] An array of indexData objects - * defining the indexes to use with the store. For every index to be used - * one indexData object needs to be passed in the array. - * An indexData object is defined as follows: - * @param {Object} [kwArgs.indexes.indexData] An object defining the index to - * use - * @param {String} kwArgs.indexes.indexData.name The name of the index - * @param {String} [kwArgs.indexes.indexData.keyPath] The key path of the index - * @param {Boolean} [kwArgs.indexes.indexData.unique] Whether the index is unique - * @param {Boolean} [kwArgs.indexes.indexData.multiEntry] Whether the index is multi entry - * @param {Array} [kwArgs.implementationPreference=['indexedDB','webkitIndexedDB','mozIndexedDB','shimIndexedDB']] An array of strings naming implementations to be used, in order or preference - * @param {Function} [onStoreReady] A callback to be called when the store - * is ready to be used. - * @example - // create a store for customers with an additional index over the - // `lastname` property. - var myCustomerStore = new IDBStore({ - dbVersion: 1, - storeName: 'customer-index', - keyPath: 'customerid', - autoIncrement: true, - onStoreReady: populateTable, - indexes: [ - { name: 'lastname', keyPath: 'lastname', unique: false, multiEntry: false } - ] - }); - * @example - // create a generic store - var myCustomerStore = new IDBStore({ - storeName: 'my-data-store', - onStoreReady: function(){ - // start working with the store. - } - }); - */ - var IDBStore = function (kwArgs, onStoreReady) { +(function(name, definition, global) { - if (typeof onStoreReady == 'undefined' && typeof kwArgs == 'function') { - onStoreReady = kwArgs; - } - if (Object.prototype.toString.call(kwArgs) != '[object Object]') { - kwArgs = {}; - } + 'use strict'; - for (var key in defaults) { - this[key] = typeof kwArgs[key] != 'undefined' ? kwArgs[key] : defaults[key]; + if (typeof define === 'function') { + define(definition); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = definition(); + } else { + global[name] = definition(); } +})('IDBStore', function() { - this.dbName = this.storePrefix + this.storeName; - this.dbVersion = parseInt(this.dbVersion, 10) || 1; - - onStoreReady && (this.onStoreReady = onStoreReady); - - var env = typeof window == 'object' ? window : self; - var availableImplementations = this.implementationPreference.filter(function (implName) { - return implName in env; - }); - this.implementation = availableImplementations[0]; - this.idb = env[this.implementation]; - this.keyRange = env.IDBKeyRange || env.webkitIDBKeyRange || env.mozIDBKeyRange; - - this.consts = { - 'READ_ONLY': 'readonly', - 'READ_WRITE': 'readwrite', - 'VERSION_CHANGE': 'versionchange', - 'NEXT': 'next', - 'NEXT_NO_DUPLICATE': 'nextunique', - 'PREV': 'prev', - 'PREV_NO_DUPLICATE': 'prevunique' - }; - - this.openDB(); - }; - - /** @lends IDBStore.prototype */ - var proto = { - - /** - * A pointer to the IDBStore ctor - * - * @private - * @type {Function} - * @constructs - */ - constructor: IDBStore, - - /** - * The version of IDBStore - * - * @type {String} - */ - version: '1.6.2', - - /** - * A reference to the IndexedDB object - * - * @type {Object} - */ - db: null, - - /** - * The full name of the IndexedDB used by IDBStore, composed of - * this.storePrefix + this.storeName - * - * @type {String} - */ - dbName: null, - - /** - * The version of the IndexedDB used by IDBStore - * - * @type {Number} - */ - dbVersion: null, - - /** - * A reference to the objectStore used by IDBStore - * - * @type {Object} - */ - store: null, - - /** - * The store name - * - * @type {String} - */ - storeName: null, - - /** - * The prefix to prepend to the store name - * - * @type {String} - */ - storePrefix: null, - - /** - * The key path - * - * @type {String} - */ - keyPath: null, - - /** - * Whether IDBStore uses autoIncrement - * - * @type {Boolean} - */ - autoIncrement: null, + 'use strict'; - /** - * The indexes used by IDBStore - * - * @type {Array} - */ - indexes: null, - - /** - * The implemantations to try to use, in order of preference - * - * @type {Array} - */ - implementationPreference: null, - - /** - * The actual implementation being used - * - * @type {String} - */ - implementation: '', + var defaultErrorHandler = function(error) { + throw error; + }; + var defaultSuccessHandler = function() { + }; - /** - * The callback to be called when the store is ready to be used - * - * @type {Function} - */ - onStoreReady: null, + var defaults = { + dbName: 'IDBWrapper', + dbVersion: 1, + stores: [{ + name: 'Store', + keyPath: 'id', + autoIncrement: true, + indexes: [] + }], + implementationPreference: [ + 'indexedDB', + 'webkitIndexedDB', + 'mozIndexedDB', + 'shimIndexedDB' + ], + onDbReady: function() { + }, + onError: defaultErrorHandler + }; - /** - * The callback to be called if an error occurred during instantiation - * of the store - * - * @type {Function} - */ - onError: null, + var storeDefaults = { + name: 'Store', + keyPath: 'id', + autoIncrement: true, + indexes: [] + }; - /** - * The internal insertID counter - * - * @type {Number} - * @private - */ - _insertIdCount: 0, + var consts = { + 'READ_ONLY': 'readonly', + 'READ_WRITE': 'readwrite', + 'VERSION_CHANGE': 'versionchange', + 'NEXT': 'next', + 'NEXT_NO_DUPLICATE': 'nextunique', + 'PREV': 'prev', + 'PREV_NO_DUPLICATE': 'prevunique' + }; /** - * Opens an IndexedDB; called by the constructor. * - * Will check if versions match and compare provided index configuration - * with existing ones, and update indexes if necessary. + * The IDBStore constructor * - * Will call this.onStoreReady() if everything went well and the store - * is ready to use, and this.onError() is something went wrong. - * - * @private + * @constructor + * @name IDBStore + * @version 1.6.2 * + * @param {Object} [kwArgs] An options object used to configure the store and + * set callbacks + * @param {Object} [kwArgs.stores=[ ... ] The stores + * @param {String} [kwArgs.dbName='IDBWrapper'] The name of the database + * @param {Number} [kwArgs.dbVersion=1] The version of the store + * @param {String} [kwArgs.keyPath='id'] The key path to use. If you want to + * setup IDBWrapper to work with out-of-line keys, you need to set this to + * `null` + * @param {Boolean} [kwArgs.autoIncrement=true] If set to true, IDBStore will + * automatically make sure a unique keyPath value is present on each object + * that is stored. + * @param {Function} [kwArgs.onDbReady] A callback to be called when the + * DB is ready to be used. + * @param {Function} [kwArgs.onError=throw] A callback to be called when an + * error occurred during instantiation of the store. + * @param {Array} [kwArgs.indexes=[]] An array of indexData objects + * defining the indexes to use with the store. For every index to be used + * one indexData object needs to be passed in the array. + * An indexData object is defined as follows: + * @param {Object} [kwArgs.indexes.indexData] An object defining the index to + * use + * @param {String} kwArgs.indexes.indexData.name The name of the index + * @param {String} [kwArgs.indexes.indexData.keyPath] The key path of the index + * @param {Boolean} [kwArgs.indexes.indexData.unique] Whether the index is unique + * @param {Boolean} [kwArgs.indexes.indexData.multiEntry] Whether the index is multi entry + * @param {Array} [kwArgs.implementationPreference=['indexedDB','webkitIndexedDB','mozIndexedDB','shimIndexedDB']] An array of strings naming implementations to be used, in order or preference + * @param {Function} [onDbReady] A callback to be called when the DB + * is ready to be used. + * @example + // create a store for customers with an additional index over the + // `lastname` property. + var myCustomerStore = new IDBStore({ + dbVersion: 1, + stores: [{ + 'customer-index', + keyPath: 'customerid', + autoIncrement: true, + indexes: [ + { name: 'lastname', keyPath: 'lastname', unique: false, multiEntry: false } + ] + }] + onDbReady: populateTables, + }); + * @example + // create a generic store + var myCustomerStore = new IDBStore({ + storeName: ['my-data-store'], + onDbReady: function(){ + // start working with the stores. + } + }); */ - openDB: function () { - - var openRequest = this.idb.open(this.dbName, this.dbVersion); - var preventSuccessCallback = false; + var IDBStore = function(kwArgs, onDbReady) { - openRequest.onerror = function (error) { - - var gotVersionErr = false; - if ('error' in error.target) { - gotVersionErr = error.target.error.name == 'VersionError'; - } else if ('errorCode' in error.target) { - gotVersionErr = error.target.errorCode == 12; + if (typeof onDbReady == 'undefined' && typeof kwArgs == 'function') { + onDbReady = kwArgs; } - - if (gotVersionErr) { - this.onError(new Error('The version number provided is lower than the existing one.')); - } else { - this.onError(error); + if (Object.prototype.toString.call(kwArgs) != '[object Object]') { + kwArgs = {}; } - }.bind(this); - openRequest.onsuccess = function (event) { - - if (preventSuccessCallback) { - return; + for (var key in defaults) { + this[key] = typeof kwArgs[key] != 'undefined' ? kwArgs[key] : defaults[key]; } - if(this.db){ - this.onStoreReady(); - return; - } + this.dbVersion = parseInt(this.dbVersion, 10) || 1; - this.db = event.target.result; + onDbReady && (this.onDbReady = onDbReady); - if(typeof this.db.version == 'string'){ - this.onError(new Error('The IndexedDB implementation in this browser is outdated. Please upgrade your browser.')); - return; - } + var env = typeof window == 'object' ? window : self; + var availableImplementations = this.implementationPreference.filter(function(implName) { + return implName in env; + }); + var implementation = availableImplementations[0]; + this.idb = env[implementation]; - if(!this.db.objectStoreNames.contains(this.storeName)){ - // We should never ever get here. - // Lets notify the user anyway. - this.onError(new Error('Object store couldn\'t be created.')); - return; - } + this.openDB(); + }; - var emptyTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY); - this.store = emptyTransaction.objectStore(this.storeName); - - // check indexes - var existingIndexes = Array.prototype.slice.call(this.getIndexList()); - this.indexes.forEach(function(indexData){ - var indexName = indexData.name; - - if(!indexName){ - preventSuccessCallback = true; - this.onError(new Error('Cannot create index: No index name given.')); - return; - } - - this.normalizeIndexData(indexData); - - if(this.hasIndex(indexName)){ - // check if it complies - var actualIndex = this.store.index(indexName); - var complies = this.indexComplies(actualIndex, indexData); - if(!complies){ - preventSuccessCallback = true; - this.onError(new Error('Cannot modify index "' + indexName + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.')); + /** @lends IDBStore.prototype */ + var proto = { + + /** + * A pointer to the IDBStore ctor + * + * @private + * @type {Function} + * @constructs + */ + constructor: IDBStore, + + /** + * The version of IDBStore + * + * @type {String} + */ + version: '1.6.2', + + /** + * A reference to the IndexedDB object + * + * @type {Object} + */ + db: null, + + /** + * The full name of the IndexedDB used by IDBStore + * + * @type {String} + */ + dbName: null, + + /** + * The version of the IndexedDB used by IDBStore + * + * @type {Number} + */ + dbVersion: null, + + /** + * The stores + * + * @type {Array} + */ + stores: null, + + /** + * The callback to be called when the DB is ready to be used + * + * @type {Function} + */ + onDbReady: null, + + /** + * The callback to be called if an error occurred during instantiation + * of the store + * + * @type {Function} + */ + onError: null, + + /** + * Opens an IndexedDB; called by the constructor. + * + * Will check if versions match and compare provided index configuration + * with existing ones, and update indexes if necessary. + * + * Will call this.onDbReady() if everything went well and the DB + * is ready to use, and this.onError() is something went wrong. + * + * @private + * + */ + openDB: function() { + + var openRequest = this.idb.open(this.dbName, this.dbVersion); + var preventSuccessCallback = false; + var remainingCallbacks = this.stores.length; + + openRequest.onerror = function(error) { + + var gotVersionErr = false; + if ('error' in error.target) { + gotVersionErr = error.target.error.name == 'VersionError'; + } else if ('errorCode' in error.target) { + gotVersionErr = error.target.errorCode == 12; + } + + if (gotVersionErr) { + this.onError(new Error('The version number provided is lower than the existing one.')); + } else { + this.onError(error); + } + }.bind(this); + + openRequest.onsuccess = function(event) { + + if (preventSuccessCallback) { + return; + } + + if (this.db) { + this.onDbReady(); + return; + } + + this.db = event.target.result; + + if (typeof this.db.version == 'string') { + this.onError(new Error('The IndexedDB implementation in this browser is outdated. Please upgrade your browser.')); + return; + } + + for (var i = 0; i < this.stores.length; i++) { + if (!this.db.objectStoreNames.contains(this.stores[i].name)) { + // We should never ever get here. + // Lets notify the user anyway. + this.onError(new Error('Object store couldn\'t be created.')); + return; + } + + var emptyTransaction = this.db.transaction([this.stores[i].name], consts.READ_ONLY); + var currentStore = new Store(this.db, this.stores[i], emptyTransaction.objectStore(this.stores[i].name)); + this[this.stores[i].name] = currentStore; + + // check indexes + var existingIndexes = Array.prototype.slice.call(currentStore.getIndexList()); + this.stores[i].indexes && this.stores[i].indexes.forEach(handleIndexes, this); + + if (existingIndexes.length) { + preventSuccessCallback = true; + this.onError(new Error('Cannot delete index(es) "' + existingIndexes.toString() + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.')); + } + + remainingCallbacks--; + if (!preventSuccessCallback && remainingCallbacks === 0) { + this.onDbReady(); + } + } + + var self = this; + function handleIndexes(indexData) { + var indexName = indexData.name; + + if (!indexName) { + preventSuccessCallback = true; + self.onError(new Error('Cannot create index: No index name given.')); + return; + } + + currentStore.normalizeIndexData(indexData); + + if (currentStore.hasIndex(indexName)) { + // check if it complies + var actualIndex = currentStore.store.index(indexName); + var complies = currentStore.indexComplies(actualIndex, indexData); + if (!complies) { + preventSuccessCallback = true; + self.onError(new Error('Cannot modify index "' + indexName + '" for current version. Please bump version number to ' + ( self.dbVersion + 1 ) + '.')); + } + + existingIndexes.splice(existingIndexes.indexOf(indexName), 1); + } else { + preventSuccessCallback = true; + self.onError(new Error('Cannot create new index "' + indexName + '" for current version. Please bump version number to ' + ( self.dbVersion + 1 ) + '.')); + } + } + + }.bind(this); + + openRequest.onupgradeneeded = function(/* IDBVersionChangeEvent */ event) { + + this.db = event.target.result; + + for (var i = 0; i < this.stores.length; i++) { + var currentStore; + if (this.db.objectStoreNames.contains(this.stores[i].name)) { + currentStore = new Store(this.db, this.stores[i], event.target.transaction.objectStore(this.stores[i].name)); + this[this.stores[i].name] = currentStore; + } else { + var optionalParameters = {autoIncrement: this.stores[i].autoIncrement ? this.stores[i].autoIncrement : storeDefaults.autoIncrement}; + if (this.stores[i].keyPath !== null) { + optionalParameters.keyPath = this.stores[i].keyPath ? this.stores[i].keyPath : storeDefaults.keyPath; + } + window.console.log('opt kp' + optionalParameters.keyPath); + window.console.log('opt ai' + optionalParameters.autoIncrement); + window.console.log('name ' + this.stores[i].name); + currentStore = new Store(this.db, this.stores[i], this.db.createObjectStore(this.stores[i].name, optionalParameters)); + this[this.stores[i].name] = currentStore; + } + + var existingIndexes = Array.prototype.slice.call(currentStore.getIndexList()); + this.stores[i].indexes && this.stores[i].indexes.forEach(handleIndexes, this); + + if (existingIndexes.length) { + existingIndexes.forEach(handleExistingIndexes, this); + } + } + + var self = this; + function handleIndexes(indexData) { + var indexName = indexData.name; + + if (!indexName) { + preventSuccessCallback = true; + self.onError(new Error('Cannot create index: No index name given.')); + } + + currentStore.normalizeIndexData(indexData); + + if (currentStore.hasIndex(indexName)) { + // check if it complies + var actualIndex = currentStore.store.index(indexName); + var complies = self.stores[i].indexComplies(actualIndex, indexData); + if (!complies) { + // index differs, need to delete and re-create + currentStore.store.deleteIndex(indexName); + currentStore.store.createIndex(indexName, indexData.keyPath, { + unique: indexData.unique, + multiEntry: indexData.multiEntry + }); + } + + existingIndexes.splice(existingIndexes.indexOf(indexName), 1); + } else { + currentStore.store.createIndex(indexName, indexData.keyPath, { + unique: indexData.unique, + multiEntry: indexData.multiEntry + }); + } + } + + function handleExistingIndexes(_indexName) { + currentStore.deleteIndex(_indexName); + } + + }.bind(this); + }, + + /** + * Deletes the database used for this store if the IDB implementations + * provides that functionality. + * + * @param {Function} [onSuccess] A callback that is called if deletion + * was successful. + * @param {Function} [onError] A callback that is called if deletion + * failed. + */ + deleteDatabase: function(onSuccess, onError) { + if (this.idb.deleteDatabase) { + this.db.close(); + var deleteRequest = this.idb.deleteDatabase(this.dbName); + deleteRequest.onsuccess = onSuccess; + deleteRequest.onerror = onError; + } else { + onError(new Error('Browser does not support IndexedDB deleteDatabase!')); } - - existingIndexes.splice(existingIndexes.indexOf(indexName), 1); - } else { - preventSuccessCallback = true; - this.onError(new Error('Cannot create new index "' + indexName + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.')); - } - - }, this); - - if (existingIndexes.length) { - preventSuccessCallback = true; - this.onError(new Error('Cannot delete index(es) "' + existingIndexes.toString() + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.')); } - preventSuccessCallback || this.onStoreReady(); - }.bind(this); - - openRequest.onupgradeneeded = function(/* IDBVersionChangeEvent */ event){ + }; - this.db = event.target.result; + /** helpers **/ + var empty = {}; - if(this.db.objectStoreNames.contains(this.storeName)){ - this.store = event.target.transaction.objectStore(this.storeName); - } else { - var optionalParameters = { autoIncrement: this.autoIncrement }; - if (this.keyPath !== null) { - optionalParameters.keyPath = this.keyPath; - } - this.store = this.db.createObjectStore(this.storeName, optionalParameters); - } - - var existingIndexes = Array.prototype.slice.call(this.getIndexList()); - this.indexes.forEach(function(indexData){ - var indexName = indexData.name; - - if(!indexName){ - preventSuccessCallback = true; - this.onError(new Error('Cannot create index: No index name given.')); - } - - this.normalizeIndexData(indexData); - - if(this.hasIndex(indexName)){ - // check if it complies - var actualIndex = this.store.index(indexName); - var complies = this.indexComplies(actualIndex, indexData); - if(!complies){ - // index differs, need to delete and re-create - this.store.deleteIndex(indexName); - this.store.createIndex(indexName, indexData.keyPath, { unique: indexData.unique, multiEntry: indexData.multiEntry }); + function mixin(target, source) { + var name, s; + for (name in source) { + s = source[name]; + if (s !== empty[name] && s !== target[name]) { + target[name] = s; } - - existingIndexes.splice(existingIndexes.indexOf(indexName), 1); - } else { - this.store.createIndex(indexName, indexData.keyPath, { unique: indexData.unique, multiEntry: indexData.multiEntry }); - } - - }, this); - - if (existingIndexes.length) { - existingIndexes.forEach(function(_indexName){ - this.store.deleteIndex(_indexName); - }, this); } + return target; + } - }.bind(this); - }, + IDBStore.prototype = proto; + IDBStore.version = proto.version; - /** - * Deletes the database used for this store if the IDB implementations - * provides that functionality. - * - * @param {Function} [onSuccess] A callback that is called if deletion - * was successful. - * @param {Function} [onError] A callback that is called if deletion - * failed. - */ - deleteDatabase: function (onSuccess, onError) { - if (this.idb.deleteDatabase) { - this.db.close(); - var deleteRequest = this.idb.deleteDatabase(this.dbName); - deleteRequest.onsuccess = onSuccess; - deleteRequest.onerror = onError; - } else { - onError(new Error('Browser does not support IndexedDB deleteDatabase!')); - } - }, + return IDBStore; /********************* * data manipulation * *********************/ - /** - * Puts an object into the store. If an entry with the given id exists, - * it will be overwritten. This method has a different signature for inline - * keys and out-of-line keys; please see the examples below. - * - * @param {*} [key] The key to store. This is only needed if IDBWrapper - * is set to use out-of-line keys. For inline keys - the default scenario - - * this can be omitted. - * @param {Object} value The data object to store. - * @param {Function} [onSuccess] A callback that is called if insertion - * was successful. - * @param {Function} [onError] A callback that is called if insertion - * failed. - * @returns {IDBTransaction} The transaction used for this operation. - * @example - // Storing an object, using inline keys (the default scenario): - var myCustomer = { - customerid: 2346223, - lastname: 'Doe', - firstname: 'John' - }; - myCustomerStore.put(myCustomer, mySuccessHandler, myErrorHandler); - // Note that passing success- and error-handlers is optional. - * @example - // Storing an object, using out-of-line keys: - var myCustomer = { - lastname: 'Doe', - firstname: 'John' - }; - myCustomerStore.put(2346223, myCustomer, mySuccessHandler, myErrorHandler); - // Note that passing success- and error-handlers is optional. - */ - put: function (key, value, onSuccess, onError) { - if (this.keyPath !== null) { - onError = onSuccess; - onSuccess = value; - value = key; - } - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - - var hasSuccess = false, - result = null, - putRequest; - - var putTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE); - putTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - putTransaction.onabort = onError; - putTransaction.onerror = onError; - - if (this.keyPath !== null) { // in-line keys - this._addIdPropertyIfNeeded(value); - putRequest = putTransaction.objectStore(this.storeName).put(value); - } else { // out-of-line keys - putRequest = putTransaction.objectStore(this.storeName).put(value, key); - } - putRequest.onsuccess = function (event) { - hasSuccess = true; - result = event.target.result; - }; - putRequest.onerror = onError; - - return putTransaction; - }, - - /** - * Retrieves an object from the store. If no entry exists with the given id, - * the success handler will be called with null as first and only argument. - * - * @param {*} key The id of the object to fetch. - * @param {Function} [onSuccess] A callback that is called if fetching - * was successful. Will receive the object as only argument. - * @param {Function} [onError] A callback that will be called if an error - * occurred during the operation. - * @returns {IDBTransaction} The transaction used for this operation. - */ - get: function (key, onSuccess, onError) { - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - - var hasSuccess = false, - result = null; - - var getTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY); - getTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - getTransaction.onabort = onError; - getTransaction.onerror = onError; - var getRequest = getTransaction.objectStore(this.storeName).get(key); - getRequest.onsuccess = function (event) { - hasSuccess = true; - result = event.target.result; - }; - getRequest.onerror = onError; - - return getTransaction; - }, + function Store(db, params, store) { + /** + * A reference to the objectStore used by IDBStore + * + * @type {Object} + */ + this.store = null; + + /** + * The key path + * + * @type {String} + */ + this.keyPath = null; + + /** + * Whether IDBStore uses autoIncrement + * + * @type {Boolean} + */ + this.autoIncrement = null; + + /** + * The indexes used by IDBStore + * + * @type {Array} + */ + this.indexes = null; + + /** + * The implemantations to try to use, in order of preference + * + * @type {Array} + */ + this.implementationPreference = null; + + /** + * The actual implementation being used + * + * @type {String} + */ + this.implementation = ''; + + /** + * The internal insertID counter + * + * @type {Number} + * @private + */ + this._insertIdCount = 0; + + this.db = db; + this.store = store; + + var env = typeof window == 'object' ? window : self; + this.keyRange = env.IDBKeyRange || env.webkitIDBKeyRange || env.mozIDBKeyRange; + + for (var key in storeDefaults) { + this[key] = typeof params[key] != 'undefined' ? params[key] : storeDefaults[key]; + } - /** - * Removes an object from the store. - * - * @param {*} key The id of the object to remove. - * @param {Function} [onSuccess] A callback that is called if the removal - * was successful. - * @param {Function} [onError] A callback that will be called if an error - * occurred during the operation. - * @returns {IDBTransaction} The transaction used for this operation. - */ - remove: function (key, onSuccess, onError) { - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - - var hasSuccess = false, - result = null; - - var removeTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE); - removeTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - removeTransaction.onabort = onError; - removeTransaction.onerror = onError; - - var deleteRequest = removeTransaction.objectStore(this.storeName)['delete'](key); - deleteRequest.onsuccess = function (event) { - hasSuccess = true; - result = event.target.result; - }; - deleteRequest.onerror = onError; - - return removeTransaction; - }, + /** + * Puts an object into the store. If an entry with the given id exists, + * it will be overwritten. This method has a different signature for inline + * keys and out-of-line keys; please see the examples below. + * + * @param {*} [key] The key to store. This is only needed if IDBWrapper + * is set to use out-of-line keys. For inline keys - the default scenario - + * this can be omitted. + * @param {Object} value The data object to store. + * @param {Function} [onSuccess] A callback that is called if insertion + * was successful. + * @param {Function} [onError] A callback that is called if insertion + * failed. + * @returns {IDBTransaction} The transaction used for this operation. + * @example + // Storing an object, using inline keys (the default scenario): + var myCustomer = { + customerid: 2346223, + lastname: 'Doe', + firstname: 'John' + }; + myCustomerStore.put(myCustomer, mySuccessHandler, myErrorHandler); + // Note that passing success- and error-handlers is optional. + * @example + // Storing an object, using out-of-line keys: + var myCustomer = { + lastname: 'Doe', + firstname: 'John' + }; + myCustomerStore.put(2346223, myCustomer, mySuccessHandler, myErrorHandler); + // Note that passing success- and error-handlers is optional. + */ + this.put = function(key, value, onSuccess, onError) { + if (this.keyPath !== null) { + onError = onSuccess; + onSuccess = value; + value = key; + } + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + + var hasSuccess = false, + result = null, + putRequest; + + var putTransaction = this.db.transaction([this.name], consts.READ_WRITE); + putTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + putTransaction.onabort = onError; + putTransaction.onerror = onError; + + if (this.keyPath !== null) { // in-line keys + this._addIdPropertyIfNeeded(value); + putRequest = putTransaction.objectStore(this.name).put(value); + } else { // out-of-line keys + putRequest = putTransaction.objectStore(this.name).put(value, key); + } + putRequest.onsuccess = function(event) { + hasSuccess = true; + result = event.target.result; + }; + putRequest.onerror = onError; - /** - * Runs a batch of put and/or remove operations on the store. - * - * @param {Array} dataArray An array of objects containing the operation to run - * and the data object (for put operations). - * @param {Function} [onSuccess] A callback that is called if all operations - * were successful. - * @param {Function} [onError] A callback that is called if an error - * occurred during one of the operations. - * @returns {IDBTransaction} The transaction used for this operation. - */ - batch: function (dataArray, onSuccess, onError) { - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - - if(Object.prototype.toString.call(dataArray) != '[object Array]'){ - onError(new Error('dataArray argument must be of type Array.')); - } else if (dataArray.length === 0) { - return onSuccess(true); - } - - var count = dataArray.length; - var called = false; - var hasSuccess = false; - - var batchTransaction = this.db.transaction([this.storeName] , this.consts.READ_WRITE); - batchTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(hasSuccess); - }; - batchTransaction.onabort = onError; - batchTransaction.onerror = onError; - - - var onItemSuccess = function () { - count--; - if (count === 0 && !called) { - called = true; - hasSuccess = true; - } - }; - - dataArray.forEach(function (operation) { - var type = operation.type; - var key = operation.key; - var value = operation.value; - - var onItemError = function (err) { - batchTransaction.abort(); - if (!called) { - called = true; - onError(err, type, key); - } + return putTransaction; }; - if (type == 'remove') { - var deleteRequest = batchTransaction.objectStore(this.storeName)['delete'](key); - deleteRequest.onsuccess = onItemSuccess; - deleteRequest.onerror = onItemError; - } else if (type == 'put') { - var putRequest; - if (this.keyPath !== null) { // in-line keys - this._addIdPropertyIfNeeded(value); - putRequest = batchTransaction.objectStore(this.storeName).put(value); - } else { // out-of-line keys - putRequest = batchTransaction.objectStore(this.storeName).put(value, key); - } - putRequest.onsuccess = onItemSuccess; - putRequest.onerror = onItemError; - } - }, this); + /** + * Retrieves an object from the store. If no entry exists with the given id, + * the success handler will be called with null as first and only argument. + * + * @param {*} key The id of the object to fetch. + * @param {Function} [onSuccess] A callback that is called if fetching + * was successful. Will receive the object as only argument. + * @param {Function} [onError] A callback that will be called if an error + * occurred during the operation. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.get = function(key, onSuccess, onError) { + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + + var hasSuccess = false, + result = null; + + var getTransaction = this.db.transaction([this.name], consts.READ_ONLY); + getTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + getTransaction.onabort = onError; + getTransaction.onerror = onError; + var getRequest = getTransaction.objectStore(this.name).get(key); + getRequest.onsuccess = function(event) { + hasSuccess = true; + result = event.target.result; + }; + getRequest.onerror = onError; - return batchTransaction; - }, + return getTransaction; + }; - /** - * Takes an array of objects and stores them in a single transaction. - * - * @param {Array} dataArray An array of objects to store - * @param {Function} [onSuccess] A callback that is called if all operations - * were successful. - * @param {Function} [onError] A callback that is called if an error - * occurred during one of the operations. - * @returns {IDBTransaction} The transaction used for this operation. - */ - putBatch: function (dataArray, onSuccess, onError) { - var batchData = dataArray.map(function(item){ - return { type: 'put', value: item }; - }); + /** + * Removes an object from the store. + * + * @param {*} key The id of the object to remove. + * @param {Function} [onSuccess] A callback that is called if the removal + * was successful. + * @param {Function} [onError] A callback that will be called if an error + * occurred during the operation. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.remove = function(key, onSuccess, onError) { + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + + var hasSuccess = false, + result = null; + + var removeTransaction = this.db.transaction([this.name], consts.READ_WRITE); + removeTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + removeTransaction.onabort = onError; + removeTransaction.onerror = onError; + + var deleteRequest = removeTransaction.objectStore(this.name)['delete'](key); + deleteRequest.onsuccess = function(event) { + hasSuccess = true; + result = event.target.result; + }; + deleteRequest.onerror = onError; - return this.batch(batchData, onSuccess, onError); - }, + return removeTransaction; + }; - /** - * Like putBatch, takes an array of objects and stores them in a single - * transaction, but allows processing of the result values. Returns the - * processed records containing the key for newly created records to the - * onSuccess calllback instead of only returning true or false for success. - * In addition, added the option for the caller to specify a key field that - * should be set to the newly created key. - * - * @param {Array} dataArray An array of objects to store - * @param {Object} [options] An object containing optional options - * @param {String} [options.keyField=this.keyPath] Specifies a field in the record to update - * with the auto-incrementing key. Defaults to the store's keyPath. - * @param {Function} [onSuccess] A callback that is called if all operations - * were successful. - * @param {Function} [onError] A callback that is called if an error - * occurred during one of the operations. - * @returns {IDBTransaction} The transaction used for this operation. - * - */ - upsertBatch: function (dataArray, options, onSuccess, onError) { - // handle `dataArray, onSuccess, onError` signature - if (typeof options == 'function') { - onSuccess = options; - onError = onSuccess; - options = {}; - } - - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - options || (options = {}); - - if (Object.prototype.toString.call(dataArray) != '[object Array]') { - onError(new Error('dataArray argument must be of type Array.')); - } - - var keyField = options.keyField || this.keyPath; - var count = dataArray.length; - var called = false; - var hasSuccess = false; - var index = 0; // assume success callbacks are executed in order - - var batchTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE); - batchTransaction.oncomplete = function () { - if (hasSuccess) { - onSuccess(dataArray); - } else { - onError(false); - } - }; - batchTransaction.onabort = onError; - batchTransaction.onerror = onError; - - var onItemSuccess = function (event) { - var record = dataArray[index++]; - record[keyField] = event.target.result; - - count--; - if (count === 0 && !called) { - called = true; - hasSuccess = true; - } - }; + /** + * Runs a batch of put and/or remove operations on the store. + * + * @param {Array} dataArray An array of objects containing the operation to run + * and the data object (for put operations). + * @param {Function} [onSuccess] A callback that is called if all operations + * were successful. + * @param {Function} [onError] A callback that is called if an error + * occurred during one of the operations. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.batch = function(dataArray, onSuccess, onError) { + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + + if (Object.prototype.toString.call(dataArray) != '[object Array]') { + onError(new Error('dataArray argument must be of type Array.')); + } else if (dataArray.length === 0) { + return onSuccess(true); + } - dataArray.forEach(function (record) { - var key = record.key; + var count = dataArray.length; + var called = false; + var hasSuccess = false; + + var batchTransaction = this.db.transaction([this.name], consts.READ_WRITE); + batchTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(hasSuccess); + }; + batchTransaction.onabort = onError; + batchTransaction.onerror = onError; + + + var onItemSuccess = function() { + count--; + if (count === 0 && !called) { + called = true; + hasSuccess = true; + } + }; + + dataArray.forEach(function(operation) { + var type = operation.type; + var key = operation.key; + var value = operation.value; + + var onItemError = function(err) { + batchTransaction.abort(); + if (!called) { + called = true; + onError(err, type, key); + } + }; + + if (type == 'remove') { + var deleteRequest = batchTransaction.objectStore(this.name)['delete'](key); + deleteRequest.onsuccess = onItemSuccess; + deleteRequest.onerror = onItemError; + } else if (type == 'put') { + var putRequest; + if (this.keyPath !== null) { // in-line keys + this._addIdPropertyIfNeeded(value); + putRequest = batchTransaction.objectStore(this.name).put(value); + } else { // out-of-line keys + putRequest = batchTransaction.objectStore(this.name).put(value, key); + } + putRequest.onsuccess = onItemSuccess; + putRequest.onerror = onItemError; + } + }, this); + + return batchTransaction; + }; - var onItemError = function (err) { - batchTransaction.abort(); - if (!called) { - called = true; - onError(err); - } + /** + * Takes an array of objects and stores them in a single transaction. + * + * @param {Array} dataArray An array of objects to store + * @param {Function} [onSuccess] A callback that is called if all operations + * were successful. + * @param {Function} [onError] A callback that is called if an error + * occurred during one of the operations. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.putBatch = function(dataArray, onSuccess, onError) { + var batchData = dataArray.map(function(item) { + return {type: 'put', value: item}; + }); + + return this.batch(batchData, onSuccess, onError); }; - var putRequest; - if (this.keyPath !== null) { // in-line keys - this._addIdPropertyIfNeeded(record); - putRequest = batchTransaction.objectStore(this.storeName).put(record); - } else { // out-of-line keys - putRequest = batchTransaction.objectStore(this.storeName).put(record, key); - } - putRequest.onsuccess = onItemSuccess; - putRequest.onerror = onItemError; - }, this); + /** + * Like putBatch, takes an array of objects and stores them in a single + * transaction, but allows processing of the result values. Returns the + * processed records containing the key for newly created records to the + * onSuccess calllback instead of only returning true or false for success. + * In addition, added the option for the caller to specify a key field that + * should be set to the newly created key. + * + * @param {Array} dataArray An array of objects to store + * @param {Object} [options] An object containing optional options + * @param {String} [options.keyField=this.keyPath] Specifies a field in the record to update + * with the auto-incrementing key. Defaults to the store's keyPath. + * @param {Function} [onSuccess] A callback that is called if all operations + * were successful. + * @param {Function} [onError] A callback that is called if an error + * occurred during one of the operations. + * @returns {IDBTransaction} The transaction used for this operation. + * + */ + this.upsertBatch = function(dataArray, options, onSuccess, onError) { + // handle `dataArray, onSuccess, onError` signature + if (typeof options == 'function') { + onSuccess = options; + onError = onSuccess; + options = {}; + } - return batchTransaction; - }, + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + options || (options = {}); - /** - * Takes an array of keys and removes matching objects in a single - * transaction. - * - * @param {Array} keyArray An array of keys to remove - * @param {Function} [onSuccess] A callback that is called if all operations - * were successful. - * @param {Function} [onError] A callback that is called if an error - * occurred during one of the operations. - * @returns {IDBTransaction} The transaction used for this operation. - */ - removeBatch: function (keyArray, onSuccess, onError) { - var batchData = keyArray.map(function(key){ - return { type: 'remove', key: key }; - }); - - return this.batch(batchData, onSuccess, onError); - }, + if (Object.prototype.toString.call(dataArray) != '[object Array]') { + onError(new Error('dataArray argument must be of type Array.')); + } - /** - * Takes an array of keys and fetches matching objects - * - * @param {Array} keyArray An array of keys identifying the objects to fetch - * @param {Function} [onSuccess] A callback that is called if all operations - * were successful. - * @param {Function} [onError] A callback that is called if an error - * occurred during one of the operations. - * @param {String} [arrayType='sparse'] The type of array to pass to the - * success handler. May be one of 'sparse', 'dense' or 'skip'. Defaults to - * 'sparse'. This parameter specifies how to handle the situation if a get - * operation did not throw an error, but there was no matching object in - * the database. In most cases, 'sparse' provides the most desired - * behavior. See the examples for details. - * @returns {IDBTransaction} The transaction used for this operation. - * @example - // given that there are two objects in the database with the keypath - // values 1 and 2, and the call looks like this: - myStore.getBatch([1, 5, 2], onError, function (data) { … }, arrayType); + var keyField = options.keyField || this.keyPath; + var count = dataArray.length; + var called = false; + var hasSuccess = false; + var index = 0; // assume success callbacks are executed in order + + var batchTransaction = this.db.transaction([this.name], consts.READ_WRITE); + batchTransaction.oncomplete = function() { + if (hasSuccess) { + onSuccess(dataArray); + } else { + onError(false); + } + }; + batchTransaction.onabort = onError; + batchTransaction.onerror = onError; + + var onItemSuccess = function(event) { + var record = dataArray[index++]; + record[keyField] = event.target.result; + + count--; + if (count === 0 && !called) { + called = true; + hasSuccess = true; + } + }; + + dataArray.forEach(function(record) { + var key = record.key; + + var onItemError = function(err) { + batchTransaction.abort(); + if (!called) { + called = true; + onError(err); + } + }; + + var putRequest; + if (this.keyPath !== null) { // in-line keys + this._addIdPropertyIfNeeded(record); + putRequest = batchTransaction.objectStore(this.name).put(record); + } else { // out-of-line keys + putRequest = batchTransaction.objectStore(this.name).put(record, key); + } + putRequest.onsuccess = onItemSuccess; + putRequest.onerror = onItemError; + }, this); + + return batchTransaction; + }; - // this is what the `data` array will be like: + /** + * Takes an array of keys and removes matching objects in a single + * transaction. + * + * @param {Array} keyArray An array of keys to remove + * @param {Function} [onSuccess] A callback that is called if all operations + * were successful. + * @param {Function} [onError] A callback that is called if an error + * occurred during one of the operations. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.removeBatch = function(keyArray, onSuccess, onError) { + var batchData = keyArray.map(function(key) { + return {type: 'remove', key: key}; + }); + + return this.batch(batchData, onSuccess, onError); + }; - // arrayType == 'sparse': - // data is a sparse array containing two entries and having a length of 3: - [Object, 2: Object] + /** + * Takes an array of keys and fetches matching objects + * + * @param {Array} keyArray An array of keys identifying the objects to fetch + * @param {Function} [onSuccess] A callback that is called if all operations + * were successful. + * @param {Function} [onError] A callback that is called if an error + * occurred during one of the operations. + * @param {String} [arrayType='sparse'] The type of array to pass to the + * success handler. May be one of 'sparse', 'dense' or 'skip'. Defaults to + * 'sparse'. This parameter specifies how to handle the situation if a get + * operation did not throw an error, but there was no matching object in + * the database. In most cases, 'sparse' provides the most desired + * behavior. See the examples for details. + * @returns {IDBTransaction} The transaction used for this operation. + * @example + // given that there are two objects in the database with the keypath + // values 1 and 2, and the call looks like this: + myStore.getBatch([1, 5, 2], onError, function (data) { … }, arrayType); + + // this is what the `data` array will be like: + + // arrayType == 'sparse': + // data is a sparse array containing two entries and having a length of 3: + [Object, 2: Object] 0: Object 2: Object length: 3 __proto__: Array[0] - // calling forEach on data will result in the callback being called two - // times, with the index parameter matching the index of the key in the - // keyArray. - - // arrayType == 'dense': - // data is a dense array containing three entries and having a length of 3, - // where data[1] is of type undefined: - [Object, undefined, Object] + // calling forEach on data will result in the callback being called two + // times, with the index parameter matching the index of the key in the + // keyArray. + + // arrayType == 'dense': + // data is a dense array containing three entries and having a length of 3, + // where data[1] is of type undefined: + [Object, undefined, Object] 0: Object 1: undefined 2: Object length: 3 __proto__: Array[0] - // calling forEach on data will result in the callback being called three - // times, with the index parameter matching the index of the key in the - // keyArray, but the second call will have undefined as first argument. + // calling forEach on data will result in the callback being called three + // times, with the index parameter matching the index of the key in the + // keyArray, but the second call will have undefined as first argument. - // arrayType == 'skip': - // data is a dense array containing two entries and having a length of 2: - [Object, Object] + // arrayType == 'skip': + // data is a dense array containing two entries and having a length of 2: + [Object, Object] 0: Object 1: Object length: 2 __proto__: Array[0] - // calling forEach on data will result in the callback being called two - // times, with the index parameter not matching the index of the key in the - // keyArray. - */ - getBatch: function (keyArray, onSuccess, onError, arrayType) { - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - arrayType || (arrayType = 'sparse'); - - if (Object.prototype.toString.call(keyArray) != '[object Array]'){ - onError(new Error('keyArray argument must be of type Array.')); - } else if (keyArray.length === 0) { - return onSuccess([]); - } - - var data = []; - var count = keyArray.length; - var called = false; - var hasSuccess = false; - var result = null; - - var batchTransaction = this.db.transaction([this.storeName] , this.consts.READ_ONLY); - batchTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - batchTransaction.onabort = onError; - batchTransaction.onerror = onError; - - var onItemSuccess = function (event) { - if (event.target.result || arrayType == 'dense') { - data.push(event.target.result); - } else if (arrayType == 'sparse') { - data.length++; - } - count--; - if (count === 0) { - called = true; - hasSuccess = true; - result = data; - } - }; - - keyArray.forEach(function (key) { + // calling forEach on data will result in the callback being called two + // times, with the index parameter not matching the index of the key in the + // keyArray. + */ + this.getBatch = function(keyArray, onSuccess, onError, arrayType) { + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + arrayType || (arrayType = 'sparse'); + + if (Object.prototype.toString.call(keyArray) != '[object Array]') { + onError(new Error('keyArray argument must be of type Array.')); + } else if (keyArray.length === 0) { + return onSuccess([]); + } - var onItemError = function (err) { - called = true; - result = err; - onError(err); - batchTransaction.abort(); + var data = []; + var count = keyArray.length; + var called = false; + var hasSuccess = false; + var result = null; + + var batchTransaction = this.db.transaction([this.name], consts.READ_ONLY); + batchTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + batchTransaction.onabort = onError; + batchTransaction.onerror = onError; + + var onItemSuccess = function(event) { + if (event.target.result || arrayType == 'dense') { + data.push(event.target.result); + } else if (arrayType == 'sparse') { + data.length++; + } + count--; + if (count === 0) { + called = true; + hasSuccess = true; + result = data; + } + }; + + keyArray.forEach(function(key) { + + var onItemError = function(err) { + called = true; + result = err; + onError(err); + batchTransaction.abort(); + }; + + var getRequest = batchTransaction.objectStore(this.name).get(key); + getRequest.onsuccess = onItemSuccess; + getRequest.onerror = onItemError; + + }, this); + + return batchTransaction; }; - var getRequest = batchTransaction.objectStore(this.storeName).get(key); - getRequest.onsuccess = onItemSuccess; - getRequest.onerror = onItemError; - - }, this); + /** + * Fetches all entries in the store. + * + * @param {Function} [onSuccess] A callback that is called if the operation + * was successful. Will receive an array of objects. + * @param {Function} [onError] A callback that will be called if an error + * occurred during the operation. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.getAll = function(onSuccess, onError) { + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + var getAllTransaction = this.db.transaction([this.name], consts.READ_ONLY); + var store = getAllTransaction.objectStore(this.name); + if (store.getAll) { + this._getAllNative(getAllTransaction, store, onSuccess, onError); + } else { + this._getAllCursor(getAllTransaction, store, onSuccess, onError); + } - return batchTransaction; - }, + return getAllTransaction; + }; - /** - * Fetches all entries in the store. - * - * @param {Function} [onSuccess] A callback that is called if the operation - * was successful. Will receive an array of objects. - * @param {Function} [onError] A callback that will be called if an error - * occurred during the operation. - * @returns {IDBTransaction} The transaction used for this operation. - */ - getAll: function (onSuccess, onError) { - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - var getAllTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY); - var store = getAllTransaction.objectStore(this.storeName); - if (store.getAll) { - this._getAllNative(getAllTransaction, store, onSuccess, onError); - } else { - this._getAllCursor(getAllTransaction, store, onSuccess, onError); - } - - return getAllTransaction; - }, + /** + * Implements getAll for IDB implementations that have a non-standard + * getAll() method. + * + * @param {Object} getAllTransaction An open READ transaction. + * @param {Object} store A reference to the store. + * @param {Function} onSuccess A callback that will be called if the + * operation was successful. + * @param {Function} onError A callback that will be called if an + * error occurred during the operation. + * @private + */ + this._getAllNative = function(getAllTransaction, store, onSuccess, onError) { + var hasSuccess = false, + result = null; + + getAllTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + getAllTransaction.onabort = onError; + getAllTransaction.onerror = onError; + + var getAllRequest = store.getAll(); + getAllRequest.onsuccess = function(event) { + hasSuccess = true; + result = event.target.result; + }; + getAllRequest.onerror = onError; + }; - /** - * Implements getAll for IDB implementations that have a non-standard - * getAll() method. - * - * @param {Object} getAllTransaction An open READ transaction. - * @param {Object} store A reference to the store. - * @param {Function} onSuccess A callback that will be called if the - * operation was successful. - * @param {Function} onError A callback that will be called if an - * error occurred during the operation. - * @private - */ - _getAllNative: function (getAllTransaction, store, onSuccess, onError) { - var hasSuccess = false, - result = null; - - getAllTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - getAllTransaction.onabort = onError; - getAllTransaction.onerror = onError; - - var getAllRequest = store.getAll(); - getAllRequest.onsuccess = function (event) { - hasSuccess = true; - result = event.target.result; - }; - getAllRequest.onerror = onError; - }, + /** + * Implements getAll for IDB implementations that do not have a getAll() + * method. + * + * @param {Object} getAllTransaction An open READ transaction. + * @param {Object} store A reference to the store. + * @param {Function} onSuccess A callback that will be called if the + * operation was successful. + * @param {Function} onError A callback that will be called if an + * error occurred during the operation. + * @private + */ + this._getAllCursor = function(getAllTransaction, store, onSuccess, onError) { + var all = [], + hasSuccess = false, + result = null; + + getAllTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + getAllTransaction.onabort = onError; + getAllTransaction.onerror = onError; + + var cursorRequest = store.openCursor(); + cursorRequest.onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + all.push(cursor.value); + cursor['continue'](); + } + else { + hasSuccess = true; + result = all; + } + }; + cursorRequest.onError = onError; + }; - /** - * Implements getAll for IDB implementations that do not have a getAll() - * method. - * - * @param {Object} getAllTransaction An open READ transaction. - * @param {Object} store A reference to the store. - * @param {Function} onSuccess A callback that will be called if the - * operation was successful. - * @param {Function} onError A callback that will be called if an - * error occurred during the operation. - * @private - */ - _getAllCursor: function (getAllTransaction, store, onSuccess, onError) { - var all = [], - hasSuccess = false, - result = null; - - getAllTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - getAllTransaction.onabort = onError; - getAllTransaction.onerror = onError; - - var cursorRequest = store.openCursor(); - cursorRequest.onsuccess = function (event) { - var cursor = event.target.result; - if (cursor) { - all.push(cursor.value); - cursor['continue'](); - } - else { - hasSuccess = true; - result = all; - } - }; - cursorRequest.onError = onError; - }, + /** + * Clears the store, i.e. deletes all entries in the store. + * + * @param {Function} [onSuccess] A callback that will be called if the + * operation was successful. + * @param {Function} [onError] A callback that will be called if an + * error occurred during the operation. + * @returns {IDBTransaction} The transaction used for this operation. + */ + this.clear = function(onSuccess, onError) { + onError || (onError = defaultErrorHandler); + onSuccess || (onSuccess = defaultSuccessHandler); + + var hasSuccess = false, + result = null; + + var clearTransaction = this.db.transaction([this.name], consts.READ_WRITE); + clearTransaction.oncomplete = function() { + var callback = hasSuccess ? onSuccess : onError; + callback(result); + }; + clearTransaction.onabort = onError; + clearTransaction.onerror = onError; + + var clearRequest = clearTransaction.objectStore(this.name).clear(); + clearRequest.onsuccess = function(event) { + hasSuccess = true; + result = event.target.result; + }; + clearRequest.onerror = onError; - /** - * Clears the store, i.e. deletes all entries in the store. - * - * @param {Function} [onSuccess] A callback that will be called if the - * operation was successful. - * @param {Function} [onError] A callback that will be called if an - * error occurred during the operation. - * @returns {IDBTransaction} The transaction used for this operation. - */ - clear: function (onSuccess, onError) { - onError || (onError = defaultErrorHandler); - onSuccess || (onSuccess = defaultSuccessHandler); - - var hasSuccess = false, - result = null; - - var clearTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE); - clearTransaction.oncomplete = function () { - var callback = hasSuccess ? onSuccess : onError; - callback(result); - }; - clearTransaction.onabort = onError; - clearTransaction.onerror = onError; - - var clearRequest = clearTransaction.objectStore(this.storeName).clear(); - clearRequest.onsuccess = function (event) { - hasSuccess = true; - result = event.target.result; - }; - clearRequest.onerror = onError; - - return clearTransaction; - }, + return clearTransaction; + }; - /** - * Checks if an id property needs to present on a object and adds one if - * necessary. - * - * @param {Object} dataObj The data object that is about to be stored - * @private - */ - _addIdPropertyIfNeeded: function (dataObj) { - if (typeof dataObj[this.keyPath] == 'undefined') { - dataObj[this.keyPath] = this._insertIdCount++ + Date.now(); - } - }, + /** + * Checks if an id property needs to present on a object and adds one if + * necessary. + * + * @param {Object} dataObj The data object that is about to be stored + * @private + */ + this._addIdPropertyIfNeeded = function(dataObj) { + if (typeof dataObj[this.keyPath] == 'undefined') { + dataObj[this.keyPath] = this._insertIdCount++ + Date.now(); + } + }; - /************ - * indexing * - ************/ + /************ + * indexing * + ************/ + + /** + * Returns a DOMStringList of index names of the store. + * + * @return {DOMStringList} The list of index names + */ + this.getIndexList = function() { + return this.store.indexNames; + }; - /** - * Returns a DOMStringList of index names of the store. - * - * @return {DOMStringList} The list of index names - */ - getIndexList: function () { - return this.store.indexNames; - }, + /** + * Checks if an index with the given name exists in the store. + * + * @param {String} indexName The name of the index to look for + * @return {Boolean} Whether the store contains an index with the given name + */ + this.hasIndex = function(indexName) { + return this.store.indexNames.contains(indexName); + }; - /** - * Checks if an index with the given name exists in the store. - * - * @param {String} indexName The name of the index to look for - * @return {Boolean} Whether the store contains an index with the given name - */ - hasIndex: function (indexName) { - return this.store.indexNames.contains(indexName); - }, + /** + * Normalizes an object containing index data and assures that all + * properties are set. + * + * @param {Object} indexData The index data object to normalize + * @param {String} indexData.name The name of the index + * @param {String} [indexData.keyPath] The key path of the index + * @param {Boolean} [indexData.unique] Whether the index is unique + * @param {Boolean} [indexData.multiEntry] Whether the index is multi entry + */ + this.normalizeIndexData = function(indexData) { + indexData.keyPath = indexData.keyPath || indexData.name; + indexData.unique = !!indexData.unique; + indexData.multiEntry = !!indexData.multiEntry; + }; - /** - * Normalizes an object containing index data and assures that all - * properties are set. - * - * @param {Object} indexData The index data object to normalize - * @param {String} indexData.name The name of the index - * @param {String} [indexData.keyPath] The key path of the index - * @param {Boolean} [indexData.unique] Whether the index is unique - * @param {Boolean} [indexData.multiEntry] Whether the index is multi entry - */ - normalizeIndexData: function (indexData) { - indexData.keyPath = indexData.keyPath || indexData.name; - indexData.unique = !!indexData.unique; - indexData.multiEntry = !!indexData.multiEntry; - }, + /** + * Checks if an actual index complies with an expected index. + * + * @param {Object} actual The actual index found in the store + * @param {Object} expected An Object describing an expected index + * @return {Boolean} Whether both index definitions are identical + */ + this.indexComplies = function(actual, expected) { + var complies = ['keyPath', 'unique', 'multiEntry'].every(function(key) { + // IE10 returns undefined for no multiEntry + if (key == 'multiEntry' && actual[key] === undefined && expected[key] === false) { + return true; + } + // Compound keys + if (key == 'keyPath' && Object.prototype.toString.call(expected[key]) == '[object Array]') { + var exp = expected.keyPath; + var act = actual.keyPath; + + // IE10 can't handle keyPath sequences and stores them as a string. + // The index will be unusable there, but let's still return true if + // the keyPath sequence matches. + if (typeof act == 'string') { + return exp.toString() == act; + } + + // Chrome/Opera stores keyPath squences as DOMStringList, Firefox + // as Array + if (!(typeof act.contains == 'function' || typeof act.indexOf == 'function')) { + return false; + } + + if (act.length !== exp.length) { + return false; + } + + for (var i = 0, m = exp.length; i < m; i++) { + if (!( (act.contains && act.contains(exp[i])) || act.indexOf(exp[i] !== -1) )) { + return false; + } + } + return true; + } + return expected[key] == actual[key]; + }); + return complies; + }; - /** - * Checks if an actual index complies with an expected index. - * - * @param {Object} actual The actual index found in the store - * @param {Object} expected An Object describing an expected index - * @return {Boolean} Whether both index definitions are identical - */ - indexComplies: function (actual, expected) { - var complies = ['keyPath', 'unique', 'multiEntry'].every(function (key) { - // IE10 returns undefined for no multiEntry - if (key == 'multiEntry' && actual[key] === undefined && expected[key] === false) { - return true; - } - // Compound keys - if (key == 'keyPath' && Object.prototype.toString.call(expected[key]) == '[object Array]') { - var exp = expected.keyPath; - var act = actual.keyPath; - - // IE10 can't handle keyPath sequences and stores them as a string. - // The index will be unusable there, but let's still return true if - // the keyPath sequence matches. - if (typeof act == 'string') { - return exp.toString() == act; - } - - // Chrome/Opera stores keyPath squences as DOMStringList, Firefox - // as Array - if ( ! (typeof act.contains == 'function' || typeof act.indexOf == 'function') ) { - return false; - } - - if (act.length !== exp.length) { - return false; - } - - for (var i = 0, m = exp.length; i Date: Sat, 9 Apr 2016 23:43:51 -0300 Subject: [PATCH 3/3] Fixing var scope bug. --- idbstore.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/idbstore.js b/idbstore.js index 84b4582..21a06d1 100644 --- a/idbstore.js +++ b/idbstore.js @@ -295,13 +295,13 @@ } } - var self = this; function handleIndexes(indexData) { + var self = this; // jshint ignore:line var indexName = indexData.name; if (!indexName) { preventSuccessCallback = true; - self.onError(new Error('Cannot create index: No index name given.')); + self.onError(new Error('Cannot create index: No index name given.')); return; } @@ -313,7 +313,7 @@ var complies = currentStore.indexComplies(actualIndex, indexData); if (!complies) { preventSuccessCallback = true; - self.onError(new Error('Cannot modify index "' + indexName + '" for current version. Please bump version number to ' + ( self.dbVersion + 1 ) + '.')); + self.onError(new Error('Cannot modify index "' + indexName + '" for current version. Please bump version number to ' + ( self.dbVersion + 1 ) + '.')); } existingIndexes.splice(existingIndexes.indexOf(indexName), 1); @@ -339,9 +339,6 @@ if (this.stores[i].keyPath !== null) { optionalParameters.keyPath = this.stores[i].keyPath ? this.stores[i].keyPath : storeDefaults.keyPath; } - window.console.log('opt kp' + optionalParameters.keyPath); - window.console.log('opt ai' + optionalParameters.autoIncrement); - window.console.log('name ' + this.stores[i].name); currentStore = new Store(this.db, this.stores[i], this.db.createObjectStore(this.stores[i].name, optionalParameters)); this[this.stores[i].name] = currentStore; } @@ -354,8 +351,8 @@ } } - var self = this; function handleIndexes(indexData) { + var self = this; // jshint ignore:line var indexName = indexData.name; if (!indexName) { @@ -368,7 +365,7 @@ if (currentStore.hasIndex(indexName)) { // check if it complies var actualIndex = currentStore.store.index(indexName); - var complies = self.stores[i].indexComplies(actualIndex, indexData); + var complies = currentStore.indexComplies(actualIndex, indexData); if (!complies) { // index differs, need to delete and re-create currentStore.store.deleteIndex(indexName);