IndexedDB handle data migration onupgradeneeded - javascript

i'm developing a offline-webapplication with IndexedDB. So I thought a lot about data migration in case of a version change.
For example, I had 3 ObjectStores in DB Version 3. Now i noticed, that i should have a specific index at all 3 ObjectStores. But its not possible to add an index afterwards to an existing ObjectStore, without losing data.
What could be the solution to handle data migration in an "onupgradeneeded"-event?

No Need to kill the StoreObject just update it like this :
request.onupgradeneeded = function(evt) {
var dataBase = evt.target.result;
var txn = evt.target.transaction;
//////////
var storeCreateIndex = function (objectStore, name, options) {
if (!objectStore.indexNames.contains(name)) {
objectStore.createIndex(name, name, options);
}
}
//////////
var catalogItem, mangaItem, chapterItem, artworkItem;
if (evt.newVersion != evt.oldVersion) {
// Get exiting objectStore
catalogItem = txn.objectStore('CatalogItem');
mangaItem = txn.objectStore('MangaItem');
chapterItem = txn.objectStore('ChapterItem');
artworkItem = txn.objectStore('ArtworkList');
} else {
// Fist creation of database objectStore
catalogItem = dataBase.db.createObjectStore("CatalogItem", { keyPath: "key" });
mangaItem = dataBase.db.createObjectStore("MangaItem", { keyPath: "key" });
chapterItem = dataBase.db.createObjectStore("ChapterItem", { keyPath: "key" });
artworkItem = dataBase.db.createObjectStore("ArtworkList", { keyPath: "key" });
}
//////////
storeCreateIndex(catalogItem, "popularity", { unique: false });
storeCreateIndex(catalogItem, "author", { unique: false });
storeCreateIndex(catalogItem, "status", { unique: false });
storeCreateIndex(catalogItem, "isFavorite", { unique: false });
storeCreateIndex(chapterItem, "isBookmarked", { unique: false });
storeCreateIndex(chapterItem, "isDownloaded", { unique: false });
}

As noted in the comments above:
Attempting to initiate a new transaction from "onupgradeneeded" will result the error:
InvalidStateError: DOM IDBDatabase Exception 11
Instead use the transaction referenced by the request object. Example:
function opendb(oncomplete){
var version = 1;
var migrateobjects = [];
var request = indexedDB.open('mydb', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
transaction = e.target.transaction;
if(db.objectStoreNames.contains('myobjects')){
migraterequest = transaction.objectStore('myobjects').openCursor();
migraterequest.onsuccess = function(e){
var cursor = e.target.result;
if (cursor){
migrateobjects.push(cursor.value);
cursor.continue();
}
};
db.deleteObjectStore('myobjects');
}
var store = db.createObjectStore('myobjects', {keyPath: 'id'});
};
request.onsuccess = function(e) {
db = e.target.result;
transaction = db.transaction('myobjects', 'readwrite')
store = transaction.objectStore('myobjects');
for(var i=0; i < migrateobjects.length; ++i)
store.put(migrateobjects[i]);
transaction.oncomplete = oncomplete;
};
};

Changing index should not clear existing records unless new index violate database constraint of existing records. But I do observe object store blow away in Chrome but not in Firefox.

Related

Get specifics ids in IndexedDB [duplicate]

I want to execute this query
select * from properties where propertyCode IN ("field1", "field2", "field3")
How can I achieve this in IndexedDB
I tried this thing
getData : function (indexName, params, objectStoreName) {
var defer = $q.defer(),
db, transaction, index, cursorRequest, request, objectStore, resultSet, dataList = [];
request = indexedDB.open('test');
request.onsuccess = function (event) {
db = request.result;
transaction = db.transaction(objectStoreName);
objectStore = transaction.objectStore(objectStoreName);
index = objectStore.index(indexName);
cursorRequest = index.openCursor(IDBKeyRange.only(params));
cursorRequest.onsuccess = function () {
resultSet = cursorRequest.result;
if(resultSet){
dataList.push(resultSet.value);
resultSet.continue();
}
else{
console.log(dataList);
defer.resolve(dataList);
}
};
cursorRequest.onerror = function (event) {
console.log('Error while opening cursor');
}
}
request.onerror = function (event) {
console.log('Not able to get access to DB in executeQuery');
}
return defer.promise;
But didn't worked. I tried google but couldn't find exact answer.
If you consider that IN is essentially equivalent to field1 == propertyCode OR field2 == propertyCode, then you could say that IN is just another way of using OR.
IndexedDB cannot do OR (unions) from a single request.
Generally, your only recourse is to do separate requests, then merge them in memory. Generally, this will not have great performance. If you are dealing with a lot of objects, you might want to consider giving up altogether on this approach and thinking of how to avoid such an approach.
Another approach is to iterate over all objects in memory, and then filter those that don't meet your conditions. Again, terrible performance.
Here is a gimmicky hack that might give you decent performance, but it requires some extra work and a tiny bit of storage overhead:
Store an extra field in your objects. For example, plan to use a property named hasPropertyCodeX.
Whenever any of the 3 properties are true (has the right code), set the field (as in, just make it a property of the object, its value is irrelevant).
When none of the 3 properties are true, delete the property from the object.
Whenever the object is modified, update the derived property (set or unset it as appropriate).
Create an index on this derived property in indexedDB.
Open a cursor over the index. Only objects with a property present will appear in the cursor results.
Example for 3rd approach
var request = indexedDB.open(...);
request.onupgradeneeded = upgrade;
function upgrade(event) {
var db = event.target.result;
var store = db.createObjectStore('store', ...);
// Create another index for the special property
var index = store.createIndex('hasPropCodeX', 'hasPropCodeX');
}
function putThing(db, thing) {
// Before storing the thing, secretly update the hasPropCodeX value
// which is derived from the thing's other properties
if(thing.field1 === 'propCode' || thing.field2 === 'propCode' ||
thing.field3 === 'propCode') {
thing.hasPropCodeX = 1;
} else {
delete thing.hasPropCodeX;
}
var tx = db.transaction('store', 'readwrite');
var store = tx.objectStore('store');
store.put(thing);
}
function getThingsWherePropCodeXInAnyof3Fields(db, callback) {
var things = [];
var tx = db.transaction('store');
var store = tx.objectStore('store');
var index = store.index('hasPropCodeX');
var request = index.openCursor();
request.onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
var thing = cursor.value;
things.push(thing);
cursor.continue();
} else {
callback(things);
}
};
request.onerror = function(event) {
console.error(event.target.error);
callback(things);
};
}
// Now that you have an api, here is some example calling code
// Not bothering to promisify it
function getData() {
var request = indexedDB.open(...);
request.onsuccess = function(event) {
var db = event.target.result;
getThingsWherePropCodeXInAnyof3Fields(db, function(things) {
console.log('Got %s things', things.length);
for(let thing of things) {
console.log('Thing', thing);
}
});
};
}

initialize array that is within a object, use in localStorage

Isolated Question:
user[username] = {
cool: true,
stuff: false,
arrData: []
};
I am trying to globally initialize my arrData array in order to populate in different scope; the below does not work.
let user.arrData = [],
later I want to do the following, for context:
...........
inputs.forEach(function(input){
user = JSON.parse(localStorage.getItem(username)); // tried moving this here
user.arrData.push({ id: input.id, checked: input.checked });
// the above errors on .arrData cannot push undefined, I have tried also including the key here, how is this done?
});
localStorage.setItem(user, JSON.stringify(user.arrData));
console.log({ arrData: user.arrData }); // returns undefined
............
Sub question: can I set and retrieve a object.array with localStorage like this. Meaning my reference to (user.arrData)?
Context/actual code:
Below is the complete relevant code for context: I am trying to save a series of checkbox states per user with local storage, then reload them when a button is clicked.
let user = {};
var savebtnBM = document.getElementById("savebtnBM");
const username = document.querySelector("span.name").textContent;
user[username] = {
cool: true,
stuff: false,
arrData: []
};
var layersMod = document.querySelectorAll(".layers #savebtnBM")[0];
layersMod.addEventListener('click', ()=>{
setTimeout(
function() {
Saver();
}, 2000);
});
function Saver() {
var inputs = document.querySelectorAll('input[type="checkbox"]');
// var arrData = [];
inputs.forEach(function(input){
user[username].arrData.push({ id: input.id, checked: input.checked });
});
localStorage.setItem(user, JSON.stringify(user.arrData));
console.log(JSON.stringify(user.arrData));
}
var etHOME= document.getElementById("et-phone-home");
etHOME.addEventListener('click', ()=>{
user = JSON.parse(localStorage.getItem(username));
console.log(user);
document.getElementsByClassName("big-button")[0].click();
setTimeout(load_, 2000);
function load_() {
var inputs = JSON.parse(localStorage.getItem(user));
inputs.forEach(function(input){
document.getElementById(input.id).checked = input.checked;
load__();
});
function load__() {
var elems = document.querySelectorAll('input:checked');
for(var i = 0; i<elems.length; i++) {
elems[i].click();
}
}
}
});
Sub question response:
The localStorage and sessionStorage can only handle strings.
So first we must convert our object to string with JSON.stringify() as in the following example:
localStorage.setItem('user', JSON.stringify(user));
When it comes to obtaining the previously saved we do the opposite, using JSON.parse():
//...
const storedUser = JSON.parse(localStorage.getItem('user'));
console.log({ arrData: storedUser.arrData });

Global variable only accessible to first function?

I have an OK understanding of JS but am ultimately still learning. I'm trying to recreate a PHP/mySQL project over to IndexedDB and can't work out why I'm seeing an error here.
The IndexedDB connection works as expected. The first function (createItem) functions fine, however the second function (getItems) is returning an error claiming that the "db" variable is undefined. It's a global variable so should be accessible by the function's scope, and the createItem function has no problem using it. Can anyone help me see what I've missed here.
// GLOBAL DB VAR
var db;
// WAIT FOR DOM
document.addEventListener("DOMContentLoaded", function(){
// IF INDEXED DB CAPABLE
if("indexedDB" in window) {
// OPEN DB
var openRequest = indexedDB.open("newTabTest",1);
// CREATE NEW / UPDATE
openRequest.onupgradeneeded = function(e) {
// Notify user here: creating database (first time use)
var thisDB = e.target.result;
// Create "items" table if it doesn't already exist
if(!thisDB.objectStoreNames.contains("items")) {
var store = thisDB.createObjectStore("items", {keyPath: "id", autoIncrement: true});
store.createIndex("name","name", {unique:true});
store.createIndex("folder","folder", {unique:false});
store.createIndex("dial","dial", {unique:false});
}
}
openRequest.onsuccess = function(e) {
// Success- set db to target result.
db = e.target.result;
}
openRequest.onerror = function(e) {
// DB ERROR :-(
}
}
},false);
// CREATE ITEM FUNCTION
function createItem(n,u,f,c) {
var transaction = db.transaction(["items"],"readwrite");
var store = transaction.objectStore("items");
var item = {
name: n,
url: u,
folder: f,
colour: c,
dial: 0,
order: 100
}
var request = store.add(item);
request.onerror = function(e) {
console.log("An error occured.");
}
request.onsuccess = function(e) {
console.log("Successfully added.")
}
};
// GET ITEM(S) FUNCTION
// Specify index and key value OR omit for all
function getItems(callback, ind, key) {
var transaction = db.transaction(["items"],"readonly");
var store = transaction.objectStore("items");
var response = [];
// If args are omitted - grab all items
if(!ind | !key) {
var cursor = store.openCursor();
cursor.onsuccess = function(e) {
var res = e.target.result;
if(res) {
var r = {
"name": res.value['name'],
"url": res.value['url'],
"folder": res.value['folder'],
"colour": res.value['colour'],
"dial": res.value['dial'],
"order": res.value['order']
};
response.push(r);
res.continue();
}
}
cursor.oncomplete = function() {
callback(response);
}
} else {
// If both args are specified query specified index
store = store.index(ind);
var range = IDBKeyRange.bound(key, key);
store.openCursor(range).onsuccess = function(e) {
var res = e.target.result;
if(res) {
var r = {
"name": res.value['name'],
"url": res.value['url'],
"folder": res.value['folder'],
"colour": res.value['colour'],
"dial": res.value['dial'],
"order": res.value['order']
};
response.push(r);
res.continue();
}
}
cursor.oncomplete = function() {
callback(response);
}
}
};
As you've figured out in comments, you do have to stick the db-dependent actions inside a function called from a success handler.
This callback-based programming quickly becomes a pain, and a common solution for that is to use promises.
However, making IndexedDB work with promises is still work-in-progress (see https://github.com/inexorabletash/indexeddb-promises if you're interested).
If your goal is to get something done (and not to learn the bare IndexedDB APIs), perhaps you'd be better off finding a wrapper library for IndexedDB (can't recommend one though, since I've not tried working seriously with IDB yet).
I would not recommend using a variable like 'db' as you have in your example. If you are new to reading and writing asynchronous Javascript, you are just going to cause yourself a lot of pain. There are better ways to do it. It takes several pages to explain and is explained in many other questions on StackOverflow, so instead, very briefly, consider rewriting your code to do something like the following:
function createItem(db, ...) {
var tx = db.transaction(...);
// ...
}
function openIndexedDBThenCreateItem(...) {
var openRequest = indexedDB.open(...);
openRequest.onsuccess = function(event) {
var db = event.target.result;
createItem(db, ...);
};
}
function getItems(db, callback, ...) {
var tx = db.transaction(...);
var items = [];
tx.oncomplete = function(event) {
callback(items);
};
// ...
var request = store.openCursor(...);
request.onsuccess = function(event) {
var request = event.target;
var cursor = event.target.result;
if(cursor) {
var item = cursor.value;
items.push(item);
cursor.continue();
}
};
}
function openIndexedDBThenGetItems(db, callback, ...) {
var openRequest = indexedDB.open(...);
openRequest.onsuccess = function(event) {
var db = event.target.result;
getItems(db, callback, ...);
};
}
Also, you don't need to wait for DOMContentLoaded to start using indexedDB. It is immediately available.
If you get the above code, then you can consider a further improvement of adding a simple helper function:
function openIndexedDB(callback) {
var openRequest = indexedDB.open(...);
openRequest.onerror = callback;
openRequest.onsuccess = callback;
}
And then rewrite the examples like this:
function openIndexedDBThenCreateItem(...) {
openIndexedDB(function onOpen(event) {
if(event.type !== 'success') {
console.error(event);
} else {
var db = event.target.result;
createItem(db, ...);
}
});
}
function openIndexedDBThenGetItems(...) {
openIndexedDB(function onOpen(event) {
if(event.type !== 'success') {
console.error(event);
} else {
var db = event.target.result;
getItems(db, ...);
}
});
}

How to get all values from indexeddb

I am working on storing some data in the indexedDb.
I have created a method which saves the data into the indexedDb. I have stored exactly 49 records. I am trying to retrieve all of them. I have written the below code for getting the values. No other code except this line exist in my js file.
function crap() {
var indexedDb = window.indexedDB || window.webkitIndexedDB || window.msIndexedDB;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
var openedDb = indexedDb && indexedDb.open;
var isIndexDbTransactionPossible = window.IDBTransaction || window.webkitIDBTransaction;
if (isIndexDbTransactionPossible) {
isIndexDbTransactionPossible.READ_WRITE = isIndexDbTransactionPossible.READ_WRITE || 'readwrite';
isIndexDbTransactionPossible.READ_ONLY = isIndexDbTransactionPossible.READ_ONLY || 'readonly';
}
var request = indexedDb.open('Offline', DB_VERSION);
request.onupgradeneeded = function(e) {
var db = e.target.result;
if (db.objectStoreNames.contains('tab')) {
db.deleteObjectStore('tab');
}
var store = db.createObjectStore('tab', {keyPath: 'id', autoIncrement: true});
};
request.onsuccess = function(e) {
console.log("DB opened");
var db = e.target.result;
var store= db.transaction('tab', IDBTransaction.READ_ONLY).objectStore('tab');
var cursor = store.openCursor();
cursor.onsuccess = function(event) {
var c = event.target.result;
if (c) {
console.log("New value")
c.continue();
}
};
};
}
I am seeing "New Value" printed 124 times. I am not sure why the cursor.continue() is not returning null after 49th attempt. Any help is much appreciated.
I am positive that this method is not called more than one time. "DB opened" is logged only one.
Just use the getAll function:
var allRecords = store.getAll();
allRecords.onsuccess = function() {
console.log(allRecords.result);
};
Read more in the documentation: Working with IndexedDB
Instead of checking readyState, just check for whether the cursor is defined in your cursor request callback. Here is an example. I modified the names of your variables slightly for clarity.
cursorRequest.onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
var value = cursor.value;
console.log('New value:', value);
cursor.continue();
} else {
// Undefined cursor. This means either no objects found,
// or no next object found
// Do not call cursor.continue(); in this else branch because
// there are no more objects over which to iterate.
// Coincidentally, this also means we are done iterating.
console.log('Finished iterating');
}
}

Cannot create second objectStore IndexedDB

I basically have this code to generate a new database with one objectStore:
DB.open_or_create({table: 'photos', key: 'url'});
And the main code:
var DB = {
open_or_create : function(p){
var openRequest = indexedDB.open("new_app",1);
openRequest.onupgradeneeded = function(e) {
var thisDB = e.target.result;
if(!thisDB.objectStoreNames.contains(p.table)) {
thisDB.createObjectStore(p.table,{ keyPath: p.key });
}
}
openRequest.onsuccess = function(e) {DBTABLES = e.target.result}
openRequest.onerror = function(e) {console.log(e)}
}
Then i tried to add a second objectStore with the command:
DB.open_or_create2({table: 'users',key: 'id'});
And basically the same code only with another database version number:
open_or_create2 : function(p){
var openRequest = indexedDB.open("new_app",2);
openRequest.onupgradeneeded = function(e) {
var thisDB = e.target.result;
if(!thisDB.objectStoreNames.contains(p.table)) {
if(p.key){
thisDB.createObjectStore(p.table,{ keyPath: p.key });
}else{
thisDB.createObjectStore(p.table)
}
}
}
openRequest.onsuccess = function(e) {DBTABLES = e.target.result}
openRequest.onerror = function(e) {console.log(e)}
}
But as i can see in the console only the first table gets created! Also when i try to add data to the patients table i get this error:
Uncaught NotFoundError: Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found.
What do i wrong? Thanks!
In the course of development you probably already increased new_app to version 2. Bump it up to version 3 in your above code and it will work. If not, add a openRequest.onblocked handler and add console.log lines to all your event handlers to see which one is getting fired.

Categories