Skip to content

Instantly share code, notes, and snippets.

@0x8801
Last active May 31, 2021 09:06
Show Gist options
  • Save 0x8801/9454490b22b3a5135fbccfe30e1ded08 to your computer and use it in GitHub Desktop.
Save 0x8801/9454490b22b3a5135fbccfe30e1ded08 to your computer and use it in GitHub Desktop.
Export Wallet by BudgetBakers records to JSON
// Source for Jake Archibald's idb https://github.com/jakearchibald/idb/blob/v3.0.2/build/idb.js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.idb = {}));
}(this, function (exports) { 'use strict';
function toArray(arr) {
return Array.prototype.slice.call(arr);
}
function promisifyRequest(request) {
return new Promise(function(resolve, reject) {
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
reject(request.error);
};
});
}
function promisifyRequestCall(obj, method, args) {
var request;
var p = new Promise(function(resolve, reject) {
request = obj[method].apply(obj, args);
promisifyRequest(request).then(resolve, reject);
});
p.request = request;
return p;
}
function promisifyCursorRequestCall(obj, method, args) {
var p = promisifyRequestCall(obj, method, args);
return p.then(function(value) {
if (!value) return;
return new Cursor(value, p.request);
});
}
function proxyProperties(ProxyClass, targetProp, properties) {
properties.forEach(function(prop) {
Object.defineProperty(ProxyClass.prototype, prop, {
get: function() {
return this[targetProp][prop];
},
set: function(val) {
this[targetProp][prop] = val;
}
});
});
}
function proxyRequestMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function(prop) {
if (!(prop in Constructor.prototype)) return;
ProxyClass.prototype[prop] = function() {
return promisifyRequestCall(this[targetProp], prop, arguments);
};
});
}
function proxyMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function(prop) {
if (!(prop in Constructor.prototype)) return;
ProxyClass.prototype[prop] = function() {
return this[targetProp][prop].apply(this[targetProp], arguments);
};
});
}
function proxyCursorRequestMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function(prop) {
if (!(prop in Constructor.prototype)) return;
ProxyClass.prototype[prop] = function() {
return promisifyCursorRequestCall(this[targetProp], prop, arguments);
};
});
}
function Index(index) {
this._index = index;
}
proxyProperties(Index, '_index', [
'name',
'keyPath',
'multiEntry',
'unique'
]);
proxyRequestMethods(Index, '_index', IDBIndex, [
'get',
'getKey',
'getAll',
'getAllKeys',
'count'
]);
proxyCursorRequestMethods(Index, '_index', IDBIndex, [
'openCursor',
'openKeyCursor'
]);
function Cursor(cursor, request) {
this._cursor = cursor;
this._request = request;
}
proxyProperties(Cursor, '_cursor', [
'direction',
'key',
'primaryKey',
'value'
]);
proxyRequestMethods(Cursor, '_cursor', IDBCursor, [
'update',
'delete'
]);
// proxy 'next' methods
['advance', 'continue', 'continuePrimaryKey'].forEach(function(methodName) {
if (!(methodName in IDBCursor.prototype)) return;
Cursor.prototype[methodName] = function() {
var cursor = this;
var args = arguments;
return Promise.resolve().then(function() {
cursor._cursor[methodName].apply(cursor._cursor, args);
return promisifyRequest(cursor._request).then(function(value) {
if (!value) return;
return new Cursor(value, cursor._request);
});
});
};
});
function ObjectStore(store) {
this._store = store;
}
ObjectStore.prototype.createIndex = function() {
return new Index(this._store.createIndex.apply(this._store, arguments));
};
ObjectStore.prototype.index = function() {
return new Index(this._store.index.apply(this._store, arguments));
};
proxyProperties(ObjectStore, '_store', [
'name',
'keyPath',
'indexNames',
'autoIncrement'
]);
proxyRequestMethods(ObjectStore, '_store', IDBObjectStore, [
'put',
'add',
'delete',
'clear',
'get',
'getAll',
'getKey',
'getAllKeys',
'count'
]);
proxyCursorRequestMethods(ObjectStore, '_store', IDBObjectStore, [
'openCursor',
'openKeyCursor'
]);
proxyMethods(ObjectStore, '_store', IDBObjectStore, [
'deleteIndex'
]);
function Transaction(idbTransaction) {
this._tx = idbTransaction;
this.complete = new Promise(function(resolve, reject) {
idbTransaction.oncomplete = function() {
resolve();
};
idbTransaction.onerror = function() {
reject(idbTransaction.error);
};
idbTransaction.onabort = function() {
reject(idbTransaction.error);
};
});
}
Transaction.prototype.objectStore = function() {
return new ObjectStore(this._tx.objectStore.apply(this._tx, arguments));
};
proxyProperties(Transaction, '_tx', [
'objectStoreNames',
'mode'
]);
proxyMethods(Transaction, '_tx', IDBTransaction, [
'abort'
]);
function UpgradeDB(db, oldVersion, transaction) {
this._db = db;
this.oldVersion = oldVersion;
this.transaction = new Transaction(transaction);
}
UpgradeDB.prototype.createObjectStore = function() {
return new ObjectStore(this._db.createObjectStore.apply(this._db, arguments));
};
proxyProperties(UpgradeDB, '_db', [
'name',
'version',
'objectStoreNames'
]);
proxyMethods(UpgradeDB, '_db', IDBDatabase, [
'deleteObjectStore',
'close'
]);
function DB(db) {
this._db = db;
}
DB.prototype.transaction = function() {
return new Transaction(this._db.transaction.apply(this._db, arguments));
};
proxyProperties(DB, '_db', [
'name',
'version',
'objectStoreNames'
]);
proxyMethods(DB, '_db', IDBDatabase, [
'close'
]);
// Add cursor iterators
// TODO: remove this once browsers do the right thing with promises
['openCursor', 'openKeyCursor'].forEach(function(funcName) {
[ObjectStore, Index].forEach(function(Constructor) {
// Don't create iterateKeyCursor if openKeyCursor doesn't exist.
if (!(funcName in Constructor.prototype)) return;
Constructor.prototype[funcName.replace('open', 'iterate')] = function() {
var args = toArray(arguments);
var callback = args[args.length - 1];
var nativeObject = this._store || this._index;
var request = nativeObject[funcName].apply(nativeObject, args.slice(0, -1));
request.onsuccess = function() {
callback(request.result);
};
};
});
});
// polyfill getAll
[Index, ObjectStore].forEach(function(Constructor) {
if (Constructor.prototype.getAll) return;
Constructor.prototype.getAll = function(query, count) {
var instance = this;
var items = [];
return new Promise(function(resolve) {
instance.iterateCursor(query, function(cursor) {
if (!cursor) {
resolve(items);
return;
}
items.push(cursor.value);
if (count !== undefined && items.length == count) {
resolve(items);
return;
}
cursor.continue();
});
});
};
});
function openDb(name, version, upgradeCallback) {
var p = promisifyRequestCall(indexedDB, 'open', [name, version]);
var request = p.request;
if (request) {
request.onupgradeneeded = function(event) {
if (upgradeCallback) {
upgradeCallback(new UpgradeDB(request.result, event.oldVersion, request.transaction));
}
};
}
return p.then(function(db) {
return new DB(db);
});
}
function deleteDb(name) {
return promisifyRequestCall(indexedDB, 'deleteDatabase', [name]);
}
exports.openDb = openDb;
exports.deleteDb = deleteDb;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Get list of tables in DB
let dbs = await window.indexedDB.databases(),
db = dbs[0], // we are exporting the 1st db, change this as per your needs
dbPromise = await idb.openDb(db.name, db.version),
tableNames = [].slice.apply(dbPromise.objectStoreNames);
// Get content from DBs
let data = await Promise.all(tableNames.map(async (tableName) => {
return {
[tableName]: await dbPromise.transaction(tableName).objectStore(tableName).getAll()
}
}));
// wallet.budgetbakers.com records are stored here
let sequence = data[2]['by-sequence'];
// group all sequences by type of record. eg are 'account', 'category', 'record', 'currency', etc
// similar to _.groupBy(sequence, 'reservedModelType')
let groupedSequence = sequence.reduce((group, item) => {
if (!group[item.reservedModelType])
group[item.reservedModelType] = [];
group[item.reservedModelType].push(item)
return group;
}, {})
// Convert the category array into a map (key is category id and value is category item itself)
groupedSequence.categoryMap = groupedSequence.Category.reduce((group, item) => {
let categoryId = item._doc_id_rev.split('::')[0];
group[categoryId] = item;
return group;
}, {})
// Sort the records by date
groupedSequence.Record = groupedSequence.Record.sort((a, b) => {
return a.reservedCreatedAt.localeCompare(b.reservedCreatedAt)
})
function mapRecordWithCategoryAndAccount(item) {
let amountSign = item.type == 0 ? 1 : -1;
let account = groupedSequence.Account.find(function(acc) {
return acc._doc_id_rev.indexOf(item.accountId) > -1;
});
return {
title: item.note ? item.note : groupedSequence.categoryMap[item.categoryId].name,
date: item.recordDate,
category: groupedSequence.categoryMap[item.categoryId].name,
amount: amountSign * item.amount/100,
account: account ? account.name : 'Other'
}
}
let records = groupedSequence.Record.map(mapRecordWithCategoryAndAccount)
JSON.stringify(records)
@0x8801
Copy link
Author

0x8801 commented Sep 6, 2019

Log in to web.budgetbakers.com and run this on the console.

The methods are extended from @harryi3t's gist here https://gist.github.com/harryi3t/8c439df8cee752f107824964a0d005fc

@HarshK99
Copy link

After copying it on the console, it shows "undefined".
Please help!

@0x8801
Copy link
Author

0x8801 commented Dec 20, 2019

@Nehull do JSON.stringify(records)

@HarshK99
Copy link

HarshK99 commented Jan 5, 2020

Thank you. Is there a way I can export it as an XLS file?

@harryi3t
Copy link

harryi3t commented Jan 6, 2020

@Nehull You can use some online service that can convert JSON to XLS.
some examples I just googled:
https://www.aconvert.com/document/json-to-xls/
https://json-csv.com/ (you can open csv in Excel)

@0x8801
Copy link
Author

0x8801 commented Jan 6, 2020

@Nehull Also if you want to download the JSON as file you can use this

function downloadObjectAsJson(exportObj, exportName){
    var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj));
    var downloadAnchorNode = document.createElement('a');
    downloadAnchorNode.setAttribute("href",     dataStr);
    downloadAnchorNode.setAttribute("download", exportName + ".json");
    document.body.appendChild(downloadAnchorNode); // required for firefox
    downloadAnchorNode.click();
    downloadAnchorNode.remove();
}

downloadObjectAsJson(records, `budgetbaker-records-${+new Date()}`)

Function from https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser

@GerritBergen
Copy link

Have you guys figured out how to get the attachments out too?

@Hammad1029
Copy link

works really good! but can you make it include the category as well for e.g food and dinks or transportation.

also btw if anyone is wondering why the hour of the time isn't correct, it's because the data is being exported in gmt +0 and your device shows according to your timezone

@emanuelgaspar
Copy link

This is great code, however it needs to account for deleted rows, which are still in the database. The fix is replacing the "let records =" line at the bottom with:

function excludeDeleted(item) { return item._deleted !== true; }

let records = groupedSequence.Record.filter(excludeDeleted).map(mapRecordWithCategoryAndAccount);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment