abyjs.db
Version:
abyjs.db - A Database with Speed and Optimization.
427 lines (378 loc) • 12.6 kB
JavaScript
const DatabaseOptions = require("../utils/DatabaseOptions")
const fs = require("fs")
const DatabaseTable = require("./DatabaseTable")
const { promisify } = require("util")
const { EventEmitter } = require("events")
const DatabaseError = require("./DatabaseError")
const Data = require("./Data")
const read = promisify(fs.readFile)
const write = promisify(fs.writeFile)
const adapters = { encoding: "utf-8" }
/**
* The main class for the database instance.
* @type {Database}
* @param options {DatabaseOptions} the options for the database instance.
* @return {Database} the newly connected database instance.
*/
class Database extends EventEmitter {
constructor(options = DatabaseOptions) {
super()
this._resolve(options)
this._tables()
/**
* Whether the database is ready.
* @type {boolean}
* @property
*/
this.ready = false
/**
* The timestamp when the database became ready state.
* @property
* @type {?number}
*/
this.readySince = null
/**
* The route manager for all the keys stored in the global database.
* @type {Map<string, string>}
* @property true
*/
this.routers = new Map()
}
/**
* Resolves the path to a table file by using the table name and file name
* @type {function}
* @param table {string} the table name.
* @param file {string} the file name.
* @return {string} the path to the file.
*/
_path(table, file) {
return `${this.path}${table}/${file}${this.extension}`
}
/**
* **Deprecated**
* @type {function}
*/
_check(path) {
return fs.existsSync(path)
}
/**
* Checks for main path to database to make sure it exists, if it doesn't it will create a new folder.
* @type {function}
*/
_create() {
if (!this._check(this.path)) {
fs.mkdirSync(this.path)
}
}
/**
* Creates the tables with given table options.
* @type {function}
*/
_tables() {
for (const i in this.tables) {
if (!this.tables[i].name || this.Regexes.TABLES_PATTERN.test(this.tables[i].name)) return;
this.tables[i] = new DatabaseTable(this.tables[i], this)
}
}
/**
* Resolves the database options that were passed to the constructor.
* @param options {object} the options that were passed.
* @type {function}
*/
_resolve(opts) {
for (const [name, value] of Object.entries(DatabaseOptions)) {
const val = opts[name]
if (val === undefined) {
this[name] = value
} else this[name] = val
}
if (!this.tables.length) throw new DatabaseError(this.Errors.TABLE_SIZE)
}
/**
* Used to send a debug event whenever the database feels like doing so.
* **This event requires `debug` option to be set to true.**
* @type {function}
* @param data {...any} the data to send to debug event.
*/
_debug(...data) {
if (this.debug) {
this.emit(this.Events.DEBUG, data.map(d => {
return typeof d === "object" ? require("util").inspect(d) : d
}).join(" "))
}
}
/**
* Parses a string into a object.
* @param readable {string} the string to read and convert from.
* @type {function}
* @return {object} the parsed object.
*/
_marshal(data) {
const start = Date.now()
data = JSON.parse(data)
this._debug(`Took ${Date.now() - start}ms to parse ${Object.keys(data).length} items.`)
return data
}
/**
* Reads from a string and attempts to find a key from given data, should not be used by public as this is meant to be used by the database.
* @type {function}
* @param data {string} Readable string to check from.
* @param key {string} the key you're looking for.
* @return {boolean} Whether the key exists in this Readable data.
*/
_readFrom(data, key) {
const regex = this._jsonKey(key)
return regex.test(data)
}
/**
* Returns the key stringified.
* @type {function}
* @param key {string} the key name.
* @return {RegExp} the created regexp.
*/
_jsonKey(key) {
return new RegExp(`(,"${key}":{|{"${key}":{)`)
}
/**
* Resolves a table by using an option.
* @return {?DatabaseTable} the table matching the query.
* @type {function}
* @param any {any} the option to search the table.
*/
_resolveTable(any) {
return
this
.tables
.find(table =>
table.name === any
)
}
/**
* Stringifies an object and returns it as string.
* @param object {object} the object to stringify.
* @return {string} the stringified object.
*/
_unmarshal(data) {
const l = Object.keys(data)
const start = Date.now()
data = JSON.stringify(data)
this._debug(`Took around ${Date.now() - start}ms to stringify ${l.length} items.`)
return data
}
/**
* Total uptime in ms for the database.
* @type {?number}
* @property
*/
get uptime() {
return Date.now() - this.readySince || null
}
/**
* The regexes for the database.
* @property
* @type {Regexes}
*/
get Regexes() {
return require("../utils/Regexes")
}
/**
* **Deprecated.**
* @property true
* @type {object}
*/
get Timer() {
let data;
return {
start() {
data = Date.now()
return true
},
end() {
return Date.now() - data || 0
}
}
}
/**
* The adapters for the database.
* @type {object}
* @property true
*/
get Adapters() {
return adapters
}
/**
* The events that the database can emit at any time.
* @type {object}
* @property true
*/
get Events() {
return {
/**
* Contains debug data, note the database will only receive bug chunks if `debug` is set to true.
* @type {event}
* @param info {string} the debug information.
* @event
*/
DEBUG: "debug",
/**
* Emitted once the database becomes ready.
* @event
* @type {event}
*/
READY: "ready",
/**
* Fired whenever a table becomes ready.
* @type {event}
* @event
* @param table {DatabaseTable} the table that became ready.
*/
TABLE_READY: "tableReady"
}
}
/**
* The errors that this database can throw.
* @type {object}
* @property true
*/
get Errors() {
return {
TABLE_NAME: "The table must have a name.",
TABLE_SIZE: "No tables were provided for the database."
}
}
/**
* Stores a key in the database.
* @type {function}
* @param table {string} the name of the table to assign the key to.
* @param key {string} the name of the key to store.
* @param value {any} the value for this key
* @param ttl {?number} the time in milliseconds which this key will expire at.
* @return {Promise<boolean>} Whether the data was successfully saved into the database.
*/
set(table, key, value, ttl) {
if (!this.tables.find(a => a.name === table)) throw new DatabaseError(`Invalid table name ${table}`)
if (typeof ttl === "number" && ttl > 0) {
ttl = Date.now() + ttl
} else ttl = undefined
const data = new Data({
key,
value,
ttl
}, this, "set")
return new Promise((resolve, reject) => {
data.resolve = resolve
data.reject = reject
this.tables.find(a => a.name === table)._set(data)
})
}
/**
* Gets key data from given table.
* @type {function}
* @param table {string} the name of the table to get the key from.
* @param key {string} the name of the key holding data.
* @return {Promise<?Data>} The data for this key, if it's got one.
*/
get(table, key) {
if (!this.tables.find(a => a.name === table)) throw new DatabaseError(`Invalid table name ${table}`)
return new Promise((resolve, reject) => {
this.tables.find(t => t.name === table)._get({
key,
resolve,
reject
})
})
}
/**
* Deletes a key from the database.
* @type {function}
* @param table {string} the name of the table holding the key.
* @param key {string} the key name to delete.
* @return {Promise<boolean>} Whether the key was successfully deleted.
*/
delete(table, key) {
if (!this.tables.find(a => a.name === table)) throw new DatabaseError(`Invalid table name ${table}`)
return new Promise((resolve, reject) => {
this.tables.find(t => t.name === table)
._delete(
{
resolve,
reject,
key
}
)
})
}
/**
* Gets all the keys & values from the database table, this will fire ttl events if any data queried is expired.
* @type {function}
* @param table {string} the name of the table.
* @param options {QueryOptions} options to pass to the all data request.
* @return {Promise<array<object>>} all the data of this table database.
* @example \n
* //request all data from "users" table. \n
* db.all("users") \n
* .then(console.log) //Array of objects. (next)
* //request all data from "users" table with a filter. \n
* db.all("users", { \n
* filter: (d) => d.data.value < 10 \n
*}) \n
*.then(console.log) //Array of objects matching filter.
*/
all(table, options = {}) {
if (!this.tables.find(a => a.name === table)) throw new DatabaseError(`Invalid table name ${table}`)
return new Promise((resolve, reject) => {
this.tables.find(t => t.name === table)
._all(
{
resolve,
reject,
options
}
)
})
}
/**
* Clears a specific table data.
* @type {function}
* @param table {string} the name of the table to reset.
* @returns {Promise<boolean>} Whether the task was executed correctly and the table got reset.
*/
clearTable(table) {}
/**
* Clears all the tables data.
* @type {function}
* @returns {Promise<boolean>} Whether the database got a successful call on deleting all the data.
*/
async clearDatabase() {
this.cache = new Map()
this.routers = new Map()
for (const table of this.tables) {
fs.rmdirSync(table.dir, {
recursive: true
})
table.cache = new Map()
table.files = new Map()
table._create()
table._availableFiles
table._cacheFiles()
}
return true
}
/**
* Connects the database and fires the ready event on successful connection.
* @type {function}
*/
connect() {
this._create(this.path)
const start = Date.now()
this._debug("Starting database with options:", this)
for (const table of this.tables) {
table._start()
}
this._debug(`Started database in ${Date.now() - start}ms`)
this.ready = true
this.readySince = Date.now()
this.emit(this.Events.READY)
}
}
module.exports = Database