123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- /*! *****************************************************************************
- Copyright (c) Microsoft Corporation.
- Permission to use, copy, modify, and/or distribute this software for any
- purpose with or without fee is hereby granted.
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- PERFORMANCE OF THIS SOFTWARE.
- ***************************************************************************** */
- var __assign = function() {
- __assign = Object.assign || function __assign(t) {
- for (var s, i = 1, n = arguments.length; i < n; i++) {
- s = arguments[i];
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
- }
- return t;
- };
- return __assign.apply(this, arguments);
- };
- function __awaiter(thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- }
- function __generator(thisArg, body) {
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
- function verb(n) { return function (v) { return step([n, v]); }; }
- function step(op) {
- if (f) throw new TypeError("Generator is already executing.");
- while (_) try {
- if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
- if (y = 0, t) op = [op[0] & 2, t.value];
- switch (op[0]) {
- case 0: case 1: t = op; break;
- case 4: _.label++; return { value: op[1], done: false };
- case 5: _.label++; y = op[1]; op = [0]; continue;
- case 7: op = _.ops.pop(); _.trys.pop(); continue;
- default:
- if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
- if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
- if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
- if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
- if (t[2]) _.ops.pop();
- _.trys.pop(); continue;
- }
- op = body.call(thisArg, _);
- } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
- if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
- }
- }
- // TODO: GLOBAL ERROR HANDLER
- // TODO: optimizing for duplicated codes, make a global promise to handle error
- // TODO: make sure that schema is sync with objectStore
- var GoDBTable = /** @class */ (function () {
- function GoDBTable(godb, name, schema) {
- this.godb = godb;
- this.name = name;
- this.schema = schema || null;
- }
- // crud(operation: GoDBTableCRUD): Promise<void | GoDBData | GoDBData[]> {
- // return new Promise((resolve, reject) => {
- // this.godb.getDB((idb) => {
- // try {
- // } catch (err) {
- // reject(err);
- // }
- // });
- // });
- // }
- // TODO: check if criteria's key fits schema
- GoDBTable.prototype.get = function (criteria) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- var store = idb
- .transaction(_this.name, 'readonly')
- .objectStore(_this.name);
- // if the key is not unique, return one object with least id
- // TODO: warning user if the key is not unique
- var onSuccess = function (e) {
- var result = e.target.result;
- resolve(result);
- };
- var onError = function (e) {
- var error = e.target.error;
- reject(error);
- };
- if (typeof criteria === 'object') {
- var key = Object.keys(criteria)[0];
- var value = criteria[key];
- var request = key === 'id'
- ? store.get(value)
- : store.index(key).get(value);
- request.onsuccess = onSuccess;
- request.onerror = onError;
- }
- else if (typeof criteria === 'number') {
- var request = store.get(criteria);
- request.onsuccess = onSuccess;
- request.onerror = onError;
- }
- else {
- reject(new Error('Table.get() failed: invalid criteria'));
- }
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- GoDBTable.prototype.getAll = function (limit) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- var store = idb
- .transaction(_this.name, 'readonly')
- .objectStore(_this.name);
- var request = limit
- ? store.getAll(null, limit)
- : store.getAll();
- request.onsuccess = function (e) {
- var result = e.target.result;
- resolve(result);
- };
- request.onerror = function (e) {
- var error = e.target.error;
- reject(error);
- };
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- // TODO: check data's schema
- // resolve: id of added item
- GoDBTable.prototype.add = function (data) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- var store = idb
- .transaction(_this.name, 'readwrite')
- .objectStore(_this.name);
- var request = store.add(data);
- // TODO: according to MDN, `onsuccess` does not exactly mean a successful adding
- request.onsuccess = function (e) {
- var result = e.target.result;
- resolve(__assign(__assign({}, data), { id: result }));
- };
- request.onerror = function (e) {
- var error = e.target.error;
- reject(error);
- };
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- // TODO FIX: the order might be unexpected when
- // `addMany` and `add` were executing at the same time
- GoDBTable.prototype.addMany = function (data) {
- var _this = this;
- return new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () {
- var arr, _i, data_1, item, _a, _b;
- return __generator(this, function (_c) {
- switch (_c.label) {
- case 0:
- if (!Array.isArray(data)) return [3 /*break*/, 5];
- arr = [];
- _i = 0, data_1 = data;
- _c.label = 1;
- case 1:
- if (!(_i < data_1.length)) return [3 /*break*/, 4];
- item = data_1[_i];
- _b = (_a = arr).push;
- return [4 /*yield*/, this.add(item)];
- case 2:
- _b.apply(_a, [_c.sent()]);
- _c.label = 3;
- case 3:
- _i++;
- return [3 /*break*/, 1];
- case 4:
- resolve(arr);
- return [3 /*break*/, 6];
- case 5:
- reject(new Error('Table.addMany() failed: input data should be an array'));
- _c.label = 6;
- case 6: return [2 /*return*/];
- }
- });
- }); });
- };
- // TODO: check data's schema
- // if data is not in table, `put` will add the data, otherwise update
- // TODO: check schema's unique key, which decides whether update or add data
- // resolve: id of updated item
- GoDBTable.prototype.put = function (data) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- if (!(data && typeof data === 'object'))
- return reject(new Error('Table.put() failed: data should be an object'));
- if (!data.id)
- return reject(new Error('Table.put() failed: id is required in data'));
- try {
- var store = idb
- .transaction(_this.name, 'readwrite')
- .objectStore(_this.name);
- // equals to `add(data)` if `data` is not in table
- var request = store.put(data);
- // TODO: according to MDN, `onsuccess` does not exactly mean a successful adding
- request.onsuccess = function (e) {
- var result = e.target.result;
- resolve(__assign(__assign({}, data), { id: result }));
- };
- request.onerror = function (e) {
- var error = e.target.error;
- reject(error);
- };
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- // update only, be ware of the difference to `put`
- GoDBTable.prototype.update = function () {
- };
- GoDBTable.prototype["delete"] = function (criteria) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- _this.get(criteria).then(function (doc) {
- var store = idb
- .transaction(_this.name, 'readwrite')
- .objectStore(_this.name);
- var request = store["delete"](doc.id);
- request.onsuccess = function () {
- resolve();
- };
- request.onerror = function (e) {
- var error = e.target.error;
- reject(error);
- };
- });
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- // find by a function
- GoDBTable.prototype.find = function (fn) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- var store = idb
- .transaction(_this.name, 'readonly')
- .objectStore(_this.name);
- store.openCursor().onsuccess = function (e) {
- var cursor = e.target.result;
- if (cursor) {
- if (fn(cursor.value))
- return resolve(cursor.value);
- cursor["continue"]();
- }
- else {
- resolve(null);
- }
- };
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- // return all results by a find function
- GoDBTable.prototype.findAll = function (fn) {
- var _this = this;
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- var store = idb
- .transaction(_this.name, 'readonly')
- .objectStore(_this.name);
- var data_2 = [];
- store.openCursor().onsuccess = function (e) {
- var cursor = e.target.result;
- if (cursor) {
- if (fn(cursor.value))
- data_2.push(cursor.value);
- cursor["continue"]();
- }
- else {
- resolve(data_2);
- }
- };
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- // TODO: people.where('age').below(22).toArray()
- GoDBTable.prototype.where = function () {
- };
- // showing 1000 items maximum in Chrome and Firefox
- GoDBTable.prototype.consoleTable = function (limit) {
- var _this = this;
- if (limit === void 0) { limit = 1000; }
- return new Promise(function (resolve, reject) {
- _this.godb.getDB(function (idb) {
- try {
- if (limit > 1000) {
- console.warn("Table.consoleTable() expects a limit no more than 1000");
- limit = 1000;
- }
- var count_1 = 0;
- var data_3 = {};
- var store = idb
- .transaction(_this.name, 'readonly')
- .objectStore(_this.name);
- store.openCursor().onsuccess = function (e) {
- var cursor = e.target.result;
- if (cursor && count_1 < limit) {
- count_1 += 1;
- data_3[cursor.key] = __assign({}, cursor.value);
- delete data_3[cursor.key].id;
- cursor["continue"]();
- }
- else {
- console.log("Data in Table['" + _this.name + "'] of Database['" + _this.godb.name + "'], limit " + limit + ":");
- console.table(data_3);
- resolve();
- }
- };
- }
- catch (err) {
- reject(err);
- }
- });
- });
- };
- return GoDBTable;
- }());
- var global = window;
- var indexedDB = global.indexedDB ||
- global.webkitIndexedDB ||
- global.mozIndexedDB ||
- global.msIndexedDB;
- var GoDB = /** @class */ (function () {
- function GoDB(name, schema, config) {
- // init params
- this.version = 0;
- this.tables = {};
- this._callbackQueue = [];
- // save database's name
- this.name = name;
- // settings
- if (config) {
- var version = config.version;
- if (version)
- this.version = version;
- }
- // init tables, `this.idb` is null when init
- this.updateSchema(schema);
- // open connection to the IndexedDB
- this.getDB();
- }
- GoDB.prototype.table = function (table, tableSchema) {
- if (!this.tables[table]) {
- this.tables[table] = new GoDBTable(this, table, tableSchema);
- this.updateSchema();
- }
- else if (tableSchema) {
- this.tables[table] = new GoDBTable(this, table, tableSchema);
- if (this._shouldUpgrade())
- this.updateSchema();
- }
- return this.tables[table];
- };
- GoDB.prototype.updateSchema = function (schema) {
- if (schema) {
- this.tables = {};
- for (var table in schema)
- this.tables[table] = new GoDBTable(this, table, schema[table]);
- }
- if (this.idb) {
- // create new objectStores when database is already opened
- console.log("Updating Schema of Database['" + this.name + "']");
- this.idb.close();
- // activate callbackQueue in getDB()
- // and avoid repeating calling _openDB() before db's opening
- this.idb = null;
- // triger `onupgradeneeded` event in _openDB() to update objectStores
- this._openDB(++this.version, true);
- }
- };
- GoDB.prototype.close = function () {
- if (this.idb) {
- this.idb.close();
- this.idb = null;
- this._connection = null;
- this._closed = true;
- console.log("A connection to Database['" + this.name + "'] is closed");
- }
- else {
- console.warn("Unable to close Database['" + this.name + "']: it is not opened yet");
- }
- };
- // drop a database
- GoDB.prototype.drop = function () {
- var _this = this;
- return new Promise(function (resolve, reject) {
- var database = _this.name;
- _this.close();
- // no need to handle Exception according to MDN
- var deleteRequest = indexedDB.deleteDatabase(database);
- deleteRequest.onsuccess = function (ev) {
- _this.version = 0;
- console.log("Database['" + database + "'] is successfully dropped");
- if (_this.onClosed) {
- if (typeof _this.onClosed === 'function')
- _this.onClosed();
- else
- console.warn("'onClosed' should be a function, not " + typeof _this.onClosed);
- _this.onClosed = null;
- }
- resolve(ev);
- };
- deleteRequest.onerror = function (ev) {
- var error = ev.target.error;
- var name = error.name, message = error.message;
- console.warn("Unable to drop Database['" + database + "'] \n- " + name + ": " + message);
- reject(name + ": " + message);
- };
- });
- };
- GoDB.prototype.getDBState = function () {
- if (this._closed)
- return 'closed';
- if (this.idb)
- return 'opened';
- if (this._connection)
- return 'connecting';
- return 'init';
- };
- /**
- * It is necessary to get `IDBDatabase` instance (`this.idb`) before
- * table operations like table.add(), table.get(), etc.
- * that's why a `getDB` function was needed in this class
- *
- * There are 4 possible states when calling `getDB`:
- *
- * State `closed`: database is closed or dropped by user
- * Operation: warning user in console, not calling callback
- * Where: After invoking `this.close()` or `this.drop()`
- *
- * State `init`: no connection yet (`this.connection` is undefined)
- * Operation: open connection to the IndexedDB database
- * Where: Constructor's `this.getDB()`, only executed once
- *
- * State `connecting`: connection opened, but db is not opened yet (`this.idb` is undefined)
- * Operation: push the table operations to a queue, executing them when db is opened
- * Where: Table operations that are in the same macrotask as `new GoDB()`
- *
- * State `opened`: The database is opened
- * Operation: Invoking the callback synchronously with `this.idb`
- * Where: Table operations when the db is opened,
- * mention that the db's opening only takes a few milliseconds,
- * and normally most of user's table operations will happen after db's opening
- *
- * State sequence:
- * init -> connecting -> opened -> closed
- *
- * Attention: callback is async-called in state `init` and `connecting`,
- * but being sync-called in state `opened`
- */
- GoDB.prototype.getDB = function (callback) {
- // State `opened`: db is opened, calling callback synchronously with IDBDatabase instance
- if (this.idb) {
- if (callback && typeof callback === 'function')
- callback(this.idb);
- return;
- }
- // State `closed`: db is closed
- if (this._closed) {
- console.warn("Database['" + this.name + "'] is closed, operations will not be executed");
- return;
- }
- // State `connecting`: connection is opened, but db is not opened yet
- if (this._connection) {
- if (callback && typeof callback === 'function')
- this._callbackQueue.push(callback);
- return;
- }
- // State `init`: opening connection to IndexedDB
- // In case user has set a version in GoDBConfig
- if (this.version)
- this._openDB(this.version);
- else
- this._openDB();
- };
- GoDB.prototype._openDB = function (version, stopUpgrade) {
- var _this = this;
- var database = this.name;
- var tables = this.tables;
- this._connection = version
- ? indexedDB.open(database, version)
- : indexedDB.open(database);
- this._connection.onsuccess = function (ev) {
- var result = _this._connection.result
- || ev.target.result;
- _this.version = result.version;
- // triggered when 'onupgradeneeded' was not called
- // meaning that the database was already existed in browser
- if (!_this.idb) {
- _this.idb = result;
- console.log("Database['" + database + "'] with version (" + result.version + ") is exisiting");
- }
- // @ts-ignore
- for (var _i = 0, _a = _this.idb.objectStoreNames; _i < _a.length; _i++) {
- var name_1 = _a[_i];
- if (!_this.tables[name_1])
- _this.tables[name_1] = new GoDBTable(_this, name_1);
- }
- // `stopUpgrade` is used to avoid infinite recursion
- if (_this._shouldUpgrade() && !stopUpgrade) {
- // make sure the objectStores structure are matching with the Schema
- _this.updateSchema();
- }
- else {
- console.log("A connection to Database['" + database + "'] is opening");
- // executing Table operations invoked by user at State `connecting`
- if (_this._callbackQueue.length) {
- _this._callbackQueue.forEach(function (fn) { return fn(_this.idb); });
- _this._callbackQueue = [];
- }
- // call onOpened if it is defined by user
- if (_this.onOpened) {
- if (typeof _this.onOpened === 'function')
- _this.onOpened(_this.idb);
- else
- console.warn("'onOpened' should be a function, not " + typeof _this.onOpened);
- _this.onOpened = null;
- }
- }
- };
- // called when db version is changing
- // it is called before `onsuccess`
- this._connection.onupgradeneeded = function (ev) {
- var oldVersion = ev.oldVersion, newVersion = ev.newVersion, target = ev.target;
- var transaction = target.transaction;
- // get IndexedDB database instance
- _this.idb = target.result;
- _this.version = newVersion;
- if (oldVersion === 0)
- console.log("Creating Database['" + database + "'] with version (" + newVersion + ")");
- // make sure the IDB objectStores structure are matching with GoDB Tables' Schema
- for (var table in tables)
- _this._updateObjectStore(table, tables[table].schema, transaction);
- if (oldVersion !== 0)
- console.log("Database['" + database + "'] version changed from (" + oldVersion + ") to (" + newVersion + ")");
- };
- this._connection.onerror = function (ev) {
- var error = ev.target.error;
- var name = error.name, message = error.message;
- throw Error("Failed to open Database['" + database + "']"
- + ("\n- " + name + ": " + message));
- };
- this._connection.onblocked = function (ev) {
- var _a = ev, newVersion = _a.newVersion, oldVersion = _a.oldVersion;
- console.warn("Database['" + database + "'] is opening somewhere with version (" + oldVersion + "),", "thus new opening request to version (" + newVersion + ") was blocked.");
- };
- };
- // check if it is needed to update the objectStores in a existing database
- // return true when objectStores structure are not matching with the schema
- // - objectStore (table) is not existing
- // - table schema has one or more index that objectStore do not have
- // - index properties are different from which defined in schema
- GoDB.prototype._shouldUpgrade = function () {
- var idb = this.idb;
- if (!idb)
- return false;
- var tables = this.tables;
- for (var table in tables) {
- // check if objectStores is existing
- if (idb.objectStoreNames.contains(table)) {
- var transaction = idb.transaction(table, 'readonly');
- var _a = transaction.objectStore(table), indexNames = _a.indexNames, keyPath = _a.keyPath, autoIncrement = _a.autoIncrement;
- // if the keyPath is not 'id', or it is not autoIncrement
- if (keyPath !== 'id' || !autoIncrement) {
- this.close();
- throw Error("The current existing objectStore '" + table + "' in IndexedDB '" + this.name + "'"
- + (" has a " + (autoIncrement ? '' : 'non-') + "autoIncrement keyPath `" + keyPath + "`,")
- + " while GoDB requires an autoIncrement keyPath `id`,"
- + (" thus GoDB can not open Database['" + this.name + "']"));
- }
- for (var index in tables[table].schema) {
- // check if the objectStore has the index
- if (indexNames.contains(index)) ;
- else {
- // closing the transaction to avoid db's `blocked` event when upgrading
- transaction.abort();
- return true;
- }
- }
- transaction.abort();
- }
- else {
- return true;
- }
- }
- return false;
- };
- // applying the schema to corresponding IndexedDB objectStores to setup indexes
- // require IDBTransaction `versionchange`
- // it can only be called in `onupgradeneeded`
- GoDB.prototype._updateObjectStore = function (table, schema, transaction) {
- var idb = this.idb;
- if (!idb)
- return;
- var putIndex = function (index, store, update) {
- if (index === 'id')
- return;
- if (update)
- store.deleteIndex(index);
- store.createIndex(index, index, {
- unique: !!schema[index]['unique'],
- multiEntry: !!schema[index]['multiEntry']
- });
- console.log((update ? 'Updating' : 'Creating') + " Index['" + index + "']", "in Table['" + table + "'], Database['" + idb.name + "']");
- };
- if (idb.objectStoreNames.contains(table)) {
- // objectStore is existing, update its indexes
- var store = transaction.objectStore(table);
- var indexNames = store.indexNames;
- // if the objectStore contains an index, update the index
- // if not, create a new index in the objectStore
- for (var index in this.tables[table].schema)
- putIndex(index, store, indexNames.contains(index));
- }
- else {
- // create objectStore if not exist
- var store = idb.createObjectStore(table, {
- keyPath: 'id',
- autoIncrement: true
- });
- console.log("Creating Table['" + table + "'] in Database['" + idb.name + "']");
- for (var index in schema)
- putIndex(index, store);
- }
- };
- return GoDB;
- }());
- export default GoDB;
|