@aegis-framework/artemis
Version:
Aegis Framework Javascript Library
345 lines (315 loc) • 11.2 kB
JavaScript
/**
* ==============================
* IndexedDB Adapter
* ==============================
*/
/**
* The IndexedDB Adapter provides the Space Class the ability to interact
* with the IndexedDB API found in most modern browsers.
*
* @class
*/
export class IndexedDB {
/**
* Create a new IndexedDB. Differently from Local and Session Storages, the
* IndexedDB Adapter requires a mandatory name, version and store name.
*
* @constructor
* @param {Object} [configuration={name = '', version = '', store = '', props = {}, index = {}}] - Configuration Object for the Adapter
* @param {string} configuration.name - Name of the Space
* @param {string} configuration.version - Version of the Space in Semantic versioning syntax
* @param {string} configuration.store - Name of the Object Store to use
* @param {Object} configuration.props - Optional Parameters for the Object Store
* @param {Object} configuration.index - Object of the indexes to declare for
* the Object Store. Each index is a JSON object with the following properties:
* @param {String} configuration.index[...].name - Name for the Index
* @param {String} configuration.index[...].field - Field on the store to apply the index to
* @param {Object} configuration.index[...].props - Index properties object
*/
constructor ({name = '', version = '', store = '', props = {}, index = {}}) {
this.name = name;
this.version = version;
this.store = store;
this.props = props || {};
this.index = index;
this.keyPath = props.keyPath || 'id';
this.upgrades = {};
if (this.version === '') {
this.numericVersion = 0;
} else {
this.numericVersion = parseInt (version.replace (/\./g, ''));
}
}
/**
* Open the Storage Object
*
* @return {Promise}
*/
open () {
if (this.name === '') {
console.error ('No name has been defined for IndexedDB space.');
return Promise.reject ();
}
if (this.store === '') {
console.error ('No store has been defined for IndexedDB space.');
return Promise.reject ();
}
if (this.storage instanceof IDBDatabase) {
return Promise.resolve (this);
} else if (this.storage instanceof Promise) {
return this.storage;
} else {
this.storage = new Promise ((resolve, reject) => {
let upgradesToApply = [];
const storage = window.indexedDB.open (this.name, this.numericVersion);
storage.onerror = (event) => {
reject (event);
};
storage.onsuccess = (event) => {
resolve ({ storage: event.target.result, upgrades: upgradesToApply });
};
storage.onupgradeneeded = (event) => {
// If the previous version is less than one, it means that
// the database needs to be created first
if (event.oldVersion < 1) {
// Create all the needed Stores
const store = event.target.result.createObjectStore (this.store, this.props);
for (const index of Object.keys (this.index)) {
store.createIndex (this.index[index].name, this.index[index].field, this.index[index].props);
}
} else {
// Check what upgrade functions have been declared in their respective order
const availableUpgrades = Object.keys (this.upgrades).sort ();
// Find the first update that needs to be applied to the database given
// the old version it currently has.
const startFrom = availableUpgrades.findIndex (u => {
const [old, ] = u.split ('::');
return parseInt (old) === event.oldVersion;
});
if (startFrom > -1) {
upgradesToApply = availableUpgrades.slice (startFrom).filter ((u) => {
const [old, next] = u.split ('::');
return parseInt (old) < this.numericVersion && parseInt (next) <= this.numericVersion;
});
}
}
// Once the transaction is done, resolve the storage object
const transaction = event.target.transaction;
transaction.addEventListener ('success', () => {
resolve ({ storage: event.target.result, upgrades: upgradesToApply });
});
};
}).then (({ storage, upgrades }) => {
this.storage = storage;
return new Promise ((resolve) => {
const res = () => resolve (storage);
this._upgrade (upgrades, res, event);
});
});
return this.storage;
}
}
/**
* Store a key-value pair. Because of the nature of a IndexedDB Database, the
* stored values must be JSON objects.
*
* @param {string} key - Key with which this value will be saved
* @param {Object} - Value to save
* @return {Promise<Object>} - When resolved, a {key, value} object is handed
* down, when it's rejected, the event is handed down.
*/
set (key = null, value) {
return this.open ().then (() => {
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store, 'readwrite').objectStore (this.store);
let op;
if (key !== null) {
const temp = {};
temp[this.keyPath] = key;
op = transaction.put (Object.assign ({}, temp, value));
} else {
op = transaction.add (value);
}
op.addEventListener ('success', (event) => { resolve ({key: event.target.result, value: value});});
op.addEventListener ('error', (event) => {reject (event);});
});
});
}
/**
* Update a key-value pair. In difference with the set () method, the update
* method will use an Object.assign () in the case of objects so no value is
* lost.
*
* @param {string} key - Key with which this value will be saved
* @param {Object} - Value to save
* @return {Promise<Object>} - When resolved, a {key, value} object is handed
* down, when it's rejected, the event is handed down.
*/
update (key, value) {
return this.get (key).then ((currentValue) => {
// If this key did not exist on the storage, then create it using the
// set method
if (typeof currentValue === 'undefined') {
return this.set (key, value);
}
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store, 'readwrite').objectStore (this.store);
const op = transaction.put (Object.assign ({}, currentValue, value));
op.addEventListener ('success', (event) => {resolve ({key: event.target.result, value: value});});
op.addEventListener ('error', (event) => {reject (event);});
});
});
}
/**
* Retrieves a value from storage given it's key
*
* @param {string} - Key with which the value was saved
* @return {Promise<Object>} - Resolves to the retreived value or its rejected
* if it doesn't exist
*/
get (key) {
return this.open ().then (() => {
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store).objectStore (this.store);
const op = transaction.get (key);
op.addEventListener ('success', (event) => {
const value = event.target.result;
if (typeof value !== 'undefined' && value !== null) {
resolve (value);
} else {
reject ();
}
});
op.addEventListener ('error', (event) => {reject (event);});
});
});
}
/**
* Retrieves all the values in the space in a key-value JSON object
*
* @return {Promise<Object>} - Resolves to the retreived values
*/
getAll () {
return this.open ().then (() => {
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store).objectStore (this.store);
const op = transaction.getAll ();
op.addEventListener ('success', (event) => {
const results = {};
event.target.result.forEach((item) => {
const id = item[this.keyPath];
delete item[this.keyPath];
results[id] = item;
});
resolve (results);
});
op.addEventListener ('error', (event) => {reject (event);});
});
});
}
/**
* Check if the space contains a given key.
*
* @param {string} key - Key to look for.
* @return {Promise} - Promise gets resolved if it exists and rejected if it
* doesn't
*/
contains (key) {
return this.get (key).then ((keys) => {
if (keys.includes (key)) {
Promise.resolve ();
} else {
return Promise.reject ();
}
});
}
/**
* Upgrade a Space Version. Upgrades must be declared before the open ()
* method is executed.
*
* @param {string} oldVersion - The version to be upgraded
* @param {string} newVersion - The version to be upgraded to
* @param {function} callback - Function to transform the old stored values to the new version's format
* @returns {Promise}
*/
upgrade (oldVersion, newVersion, callback) {
this.upgrades[`${parseInt (oldVersion.replace (/\./g, ''))}::${parseInt (newVersion.replace (/\./g, ''))}`] = callback;
return Promise.resolve ();
}
// This function acts as a helper for the upgrade progress by executing the
// needed upgrade callbacks in the correct order and sychronously.
_upgrade (upgradesToApply, resolve, event) {
// Check if there are still upgrades to apply
if (upgradesToApply.length > 0) {
this.upgrades[upgradesToApply[0]].call (this, this, event).then (() => {
this._upgrade (upgradesToApply.slice (1), resolve, event);
}).catch ((e) => console.error (e));
} else {
resolve ();
}
}
/**
* Renaming the space is not possible with the IndexedDB adapter therefore
* this function always gets a rejection.
*
* @returns {Promise} - Result of the rename operation
*/
rename () {
return Promise.reject ();
}
/**
* Getting a key by its index is not possible in this adapter, therefore this
* function always gets rejected.
*
* @return {Promise} - Promise Rejection
*/
key () {
return Promise.reject ();
}
/**
* Return all keys stored in the space.
*
* @return {Promise<string[]>} - Array of keys
*/
keys () {
return this.open ().then (() => {
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store, 'readwrite').objectStore (this.store);
const op = transaction.getAllKeys ();
op.addEventListener ('success', (event) => {resolve (event.target.result);}, false);
op.addEventListener ('error', (event) => {reject (event);}, false);
});
});
}
/**
* Delete a value from the space given its key
*
* @param {string} key - Key of the item to delete
* @return {Promise<key, value>} - Resolves to the key and value of the deleted object
*/
remove (key) {
return this.get (key).then ((value) => {
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store, 'readwrite').objectStore (this.store);
const op = transaction.delete (key);
op.addEventListener ('success', () => {resolve (value);}, false);
op.addEventListener ('error', (event) => {reject (event);}, false);
});
});
}
/**
* Clear the entire space
*
* @return {Promise} - Result of the clear operation
*/
clear () {
return this.open ().then (() => {
return new Promise ((resolve, reject) => {
const transaction = this.storage.transaction (this.store, 'readwrite').objectStore (this.store);
const op = transaction.clear ();
op.addEventListener ('success', () => {resolve ();}, false);
op.addEventListener ('error', (event) => {reject (event);}, false);
});
});
}
}