const AppLifeCycle = require("./general_lifecyle")
const DEFAULT_STATIC_SYNC_INTERVAL = (1000*60*30)
/**
* This class provides a central point of reckonning for database interfaces.
* All modules that preform actions on behalf of the contractual modules have access to the application database interface.
* This class provides interfaes for different type of databases.
*
*
* the idea of the different types of DBs
* 1) The key value DB is an LRU in memory cache which has disk backup. Different kinds of data structures can be put there
* -- if a value is out of cache, it can be retrieved from disk. Used in song-search db. Stores files as the value.
* -- Also, it retrieves files from disk at startup and stores them into the in memory table. (A shim for something like
* -- Redis might be used.)
* 2) The session_key_value_db -- this is for taking in and validating sessions in particular. It might be useful for other
* -- data collection purposes. This is in-memory records, specifically within an LRU. There is no disk backup. But,
* -- there is redundancy on secondary machines which will hold sessions for longer periods of time and age them out
* -- gracefully unless they are accessed.
* 3) the pdb - a persistence database is a database that promises to maintain a record with some permanence. It is fronted by
* -- a key value db with ephemeral properties. But, it can be assumed that the records will be written early in the life of
* -- a data object. Aging out of the LRU quickly can be expected. And, the data location can be expected to be on a seconday
* -- machine. (User records -- for customer stats, etc. may be stored here)
* 4) the sdb - a static data base. The static db is expected to be fairly similar to a persistence DB, except that it offers the
* -- guarantee that the data items are stored on a disk local to the process which accesses the code. (This is mostly used to
* -- create a set of static assets for serving to web sessions. Ideally, the sort of caching available to nginx or other
* -- web servers might be served by this sort of store.) It is expected that the store will be loaded when the database service
* -- starts for the general case. (The general case handles as much logic a it can. Dashboard and profile assets can be loaded.
* -- these are sitewide pages or page frameworks which might be loaded with user specific data.)
*
* Note: both the persistence DB and the static DB will default to using the DB provided by `default_persistent_db` if these are not
* configured or passed into the constructor.
*
* NOTE: all the base classes in /lib return the class and do not return an instance. Explore the applications and
* the reader will find that the descendant modules export instances. The classes provided by copious-transitions must
* be extended by an application.
*
* @memberof base
*/
class DBClass extends AppLifeCycle {
//
constructor(keyValueDB,sessionKeyValueDB,persistentDB,staticDB) {
//
super()
//
this.hasher = (typeof config.global_hasher === 'function') ? config.global_hasher : (key) => { return key }
//
// EPHEMERAL (in memory will be erased when the application turns off.. BUT -- may be replicated on nodes with checkpoint writes)
this.key_value_db = keyValueDB // MEMORY BASED (SHARED MEM) but with with likely backup (size less determined)
this.session_key_value_db = !(sessionKeyValueDB) ? keyValueDB : sessionKeyValueDB // MEMORY BASED (SHARED MEM) size fixed (all back up is elastic and ephemeral)
//
// CACHEABLE (WILL WRITE TO DISK SOMEWHERE)
if ( !(persistentDB) ) { // DISK within LOCAL and PEER NODES
this.pdb = require('../custom_storage/default_persistent_db')
} else {
this.pdb = persistentDB
}
//
if ( !(staticDB) ) { // LOCAL ON DISK (LOCAL CACHE AND STARTUP CACHE)
this.sdb = require('../custom_storage/default_static_db')
} else {
this.sdb = staticDB
}
if ( this.sdb && (typeof this.sdb.setPersistence === 'function ') ) {
this.sdb.setPersistence(this.pdb)
}
if ( this.key_value_db && (typeof this.key_value_db.setPersistence === 'function ') ) {
this.key_value_db.setPersistence(this.pdb)
}
}
/**
* Calls the initialization methods for all the connection based databases interfaces.
* This includes the `key_value_db`, the persistent db (pdb), and the static db (sdb).
* The `static_sync` interval is read from the configuration.
*
* @param {object} conf
*/
initialize(conf) {
this.key_value_db.initialize(conf)
if ( this.session_key_value_db !== this.key_value_db ) {
this.session_key_value_db.initialize(conf)
}
this.pdb.initialize(conf) // initialize persitence...relative to the database
this.sdb.initialize(conf)
this.static_sync_interval = conf.static_sync ? conf.static_sync : DEFAULT_STATIC_SYNC_INTERVAL
}
/**
* The method `last_step_initalization` exists in order to give make a call available to the initialization process
* in the `user_service_class` module. This method is called at the start of `run`.s
*/
last_step_initalization() { }
//
/**
* If the hasher is not set during construction,
* then the application may use this method to set the has function for use by the db.
* @param {*} hashfn
*/
set_hasher(hashfn) {
this.hasher = hashfn
}
// KEY VALUE cache like DB in memory. e.g. an interface to shm-lru-cache
/**
* Insert or update in the key value DB
* @param {string} key
* @param {object} value -
*/
async set_key_value(key,value) { // notice this is set (the actual value is stored)
await this.key_value_db.set(key,value)
}
//
/**
* delete from the key value DB
* @param {string} token
*/
async del_key_value(token) {
await this.key_value_db.delete(token)
}
//
/**
* return the value mapped by the key in the DB.
* retun null if the value is not present.
*
* @param {string} key - key mapping to the object
* @returns {any}
*/
async get_key_value(key) {
try {
let value = await this.key_value_db.get(key)
return value
} catch (e) {
console.log(e)
return null
}
}
// --- SESSION ---
/**
* The session storage.
* Most likely the session storage will be implemented as a shared hash table.
* Some implementations may user share memory storage. And, some may use DHT (distributed hash tables).
*
* @param {string} key
* @param {object} value
* @returns {string} - the hash key for the value.
*/
async set_session_key_value(key,value) { // notice this is a hash set (a speedy check)
return await this.session_key_value_db.hash_set(key,value)
}
//
/**
* Remove the key from the hash table and free up space.
*
* @param {string} key
*/
async del_session_key_value(key) {
await this.session_key_value_db.delete(key)
}
//
/**
* Get the value from the hash table.
* The value mapped by the key is often a JSON object. If so, it should be parsed before it is returned.
*
* @param {string} key
* @returns {any} The value mapped by the key
*/
async get_session_key_value(key) {
try {
let value = await this.session_key_value_db.get(key)
return value
} catch (e) {
console.log(e)
return null
}
}
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
//
/**
* This method is for fecthing the description of fields that may occur in client forms.
* This method is called by the method used to configure the validator.
*
* @param {string} fields_key
* @returns {object}
*/
async fetch_fields(fields_key) {
//
let fields = {}
if ( fields_key ) {
let fields = await this.fetch(fields_key)
if ( (typeof fields) === 'string' ) {
fields = JSON.parse(fields)
}
}
return(fields)
}
//
/**
* This may be an implementation of get for one of the DB types. But, it may have other properties.
* This is left as an abstract entry point for an application to define.
*
* @param {string} key
* @returns {object}
*/
fetch(key) { // should be written by descendant
let data = ""
return(data)
}
//
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
/**
* Get an object from static store
* @param {string} asset
* @returns {object} -- the stored object
*/
async static_store(asset) { // for small size objects depending on the initialization parameters of the cache DB
let data = ""
if ( asset ) {
try {
data = await this.sdb.get_key_value(asset)
if ( data ) {
return(JSON.parse(data))
}
} catch(e) {
}
}
return(data)
}
/**
* put the object in the static db, which attempts to keep a copy of the asset close to the processor on its own local disks...
*
* wrap the object in a carrier, in which the object is serialized with its mime-type for future transport..
*
*
* @param {string} whokey
* @param {string} text
* @param {string} mime_type
* @param {string} extension
* @returns {boolean}
*/
async put_static_store(whokey,text,mime_type,extension) {
if ( (typeof text) !== 'string' ) {
text = JSON.stringify(text)
}
if ( this.sdb === undefined ) return
let data = {
'string' : text,
'mime_type' : mime_type
}
if ( extension && ( typeof extension === 'object' ) ) {
// don't overwrite the fields this storage is about.
if ( extension.string !== undefined ) delete extension.string
if ( extension.mime_type !== undefined ) delete extension.mime_type
data = Object.assign(data,extension)
}
await this.sdb.set_key_value(whokey,data)
}
/**
*
* @param {string} whokey
*/
async del_static_store(whokey) {
if ( this.sdb === undefined ) return
await this.sdb.del_key_value(whokey)
}
/**
*
* @param {Function} sync_function
*/
static_synchronizer(sync_function) {
if ( this.sdb === undefined ) return
if ( this.sdb.schedule === undefined ) return
this.sdb.schedule(sync_function,this.static_sync_interval)
}
// CACHE
// ---- ---- ---- ---- ---- ---- ---- ---- ----
// May use a backref
/**
*
* @param {string} key
* @param {object} data
* @param {string} back_ref
*/
async store_cache(key,data,back_ref) { // make use of the application chosen key-value implementation...
//
let ref = back_ref ? data[back_ref] : false;
if ( ref ) {
let ehash = this.hasher(key)
await this.set_key_value(ref,ehash)
await this.set_key_value(ehash,data)
} else {
//
await this.set_key_value(key,data)
//
}
}
/**
*
* @param {string} key
* @param {object} body
* @returns {object}
*/
async cache_stored(key,body) {
let ehash = this.hasher(key)
try {
let data = await this.get_key_value(ehash)
if ( data ) {
if ( (typeof data) === 'string' ) {
return JSON.parse(data)
}
return data
} else return body
} catch(e) {
return body
}
}
/**
*
* @param {string} key
* @param {object} data
* @param {string} back_ref
*/
async update_cache(key,data,back_ref) {
if ( typeof data !== 'string' ) {
data = JSON.stringify(data)
}
let ref = back_ref ? data[back_ref] : false;
if ( ref ) {
let ehash = this.get_key_value(ref)
if ( ehash !== false ) {
await this.set_key_value(ehash,data)
}
} else {
//
await this.set_key_value(key,JSON.stringify(data))
//
}
}
// ---- ---- ---- ---- ---- ---- ---- ---- ----
//
// in subclass
//
/**
* A wrapper for putting data into a table
*
* @param {object} collection - A table that will eventually contain the item
* @param {object} data - any data that might be used by a method for inserting an entry or the data to be stored
*/
store(collection,data) {}
//
/**
*
* A method for checling the existence of some object in a DB table.
*
* @param {object} collection - A table that will containing the item to be found
* @param {object} data - any data that might be used by a method for searching for an entry
* @returns {boolean}
*/
exists(collection,query_components) {
return(false)
}
//
/**
*
* @param {object} collection - A table that will containing the item to be removed
* @param {object} data - any data that might be used by a method removing an entry
*/
remove(collection,data) {}
/**
*
* This is a method for wrapping `drop` usually associated with a DB for dropping a table.
* The collection is some object (perhaps a string) identifying the object to be dropped.
*
* @param {object} collection - A table that will be dropped
* @param {object} data - any data that might be used by a method dropping a DB table.
*/
drop(collection,data) {} // drop the database connection
//
/**
* This method is made available for applications that will clean up database connections on shutdown
* or at various other times.
*
* @returns {boolean} - the extending class must implement this method
*/
disconnect() {
console.log("implement in instance")
return(false)
}
}
/**
*
* USER METHODS default implementations
*
* This class adds methods that deal directly with possible user storage and lookup tables.
* The methods defined in the class are used by the class GeneralAuth found in `general_auth.js` in particular.
*
* The default behavior is for the user to be stored in the persistence DB, which is most likely to be
* implemented by a connection to a DB service. Except there is one method `fetch_user_from_key_value_store`
* that is used to get user information from the key-value DB, which is expected to be faster (in local memory).
*
* Some applcations may want to override this class in order to change the kind of user table storage arrangement from
* what is provided here. Even without the override, the constuctor expects that objects linking to external databases to be parameters.
*
* Within the user methods, the user object, `u_data` (often) is expected to have an identity field, `_id`.
*
*/
class GeneralUserDBWrapperImpl extends GeneralDBWrapperImpl {
constructor(keyValueDB,sessionKeyValueDB,persistentDB,staticDB) {
super(keyValueDB,sessionKeyValueDB,persistentDB,staticDB)
}
/**
*
* Some applications may elect to store user in the KV store.
* That is not the default behavior. The default behavior is to store the user in the persistence DB.
*
* @param {string} key
* @returns {object|boolean} - returns false on failing to find an object
*/
async fetch_user_from_key_value_store(key) {
if ( key == undefined ) return(false);
try {
let ehash = await this.get_key_value(key)
if ( ehash === null || ehash === false ) {
return(false)
}
if ( isHex(ehash) ) { // or other format
try {
let u_data = await this.get_key_value(ehash)
if ( u_data ) {
if ( (typeof data) === 'string' ) {
return JSON.parse(data)
}
return data
}
return false
} catch (e) {
return false
}
} else {
try {
let data = ehash // user data
if ( (typeof data) === 'string' ) {
return JSON.parse(data)
}
return data
} catch (e) {
return false
}
}
} catch(e) {
return false
}
}
/**
* This is supplied to provide the parenthetical to the method `fetch_user_from_key_value_store`
* @param {string} key
* @param {object} u_data
* @returns {boolean} - false on error
*/
async put_user_into_key_value_store(key,u_data,ehash) {
if ( key == undefined ) return(false);
try {
if ( ehash !== undefined ) {
await this.set_key_value(key,ehash)
if ( typeof u_data !== 'string' ) {
u_data = JSON.stringify(u_data)
}
await this.set_key_value(ehash,u_data)
return true
} else {
if ( typeof u_data !== 'string' ) {
u_data = JSON.stringify(u_data)
}
await this.set_key_value(ehash,u_data)
return true
}
} catch(e) {
return false
}
}
/**
*
* @param {string} user_txt
* @returns {string} an unique id for a stored object
*/
id_hashing(user_txt) {
let id = this.hasher(user_txt) // or this.oracular_storage()
return id
}
// store_user
// // just work with the presistent DB
/**
*
* @param {object} u_data
* @param {string} key
* @param {Function} cb
* @returns {string} an unique id for a stored object
*/
async store_user(u_data,key,cb) { // up to the application...(?)
let id = ''
if ( key ) {
id = u_data[key]
} else {
id = u_data._id
if ( id === undefined ) {
let user_txt
if ( (typeof data) !== 'string' ) {
user_txt = JSON.stringify(u_data)
} else {
user_txt = data
}
id = await this.id_hashing(user_txt) // or this.oracular_storage()
}
}
u_data._id = id
u_data._tx_directory_ensurance = true // for the endpoint -- might not need it // m_path is user
let dont_remote = false
let result = await this.pdb.update(u_data,dont_remote,'create') // the end point will write information into files and create directories...
// a response service will react sending new information to subscribers...
if ( cb !== undefined ) cb(result)
return(id)
}
//
/**
*
* @param {string} id
* @param {Function} cb
* @returns {object} - the user object stored in the DB
*/
async fetch_user(id,cb) { // up to the application...(?) // just work with the presistent DB
return(await this.pdb.findOne(id,cb))
}
//
/**
*
* @param {object} u_data
* @param {string} id_key
* @param {Function} cb
*/
async update_user(u_data,id_key,cb) { // just work with the presistent DB
let id = ''
if ( key ) {
id = u_data[id_key]
} else {
id = u_data._id
}
if ( u_data._id == undefined ) {
u_data._id = id
}
u_data._tx_directory_ensurance = true // m_path is user
let dont_remote = false
let result = await this.pdb.update(u_data,dont_remote,'update') // the end point will write information into files and create directories...
if ( cb !== undefined ) cb(result)
}
}
module.exports = DBClass