faster.db
Version:
Easy to use, fast, async-ready JSON database
497 lines (496 loc) • 17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBackup = exports.createDatabase = void 0;
const promises_1 = require("fs/promises");
const events_1 = require("events");
/**
* Represents a database that stores data of a specific type.
* @class
* @extends EventEmitter
* @template T - The type of data that the database will store
* @example
* const db = new Database('user-database', { Name: String, ID: Number });
* db.on('error', (err) => {
* console.log(err);
* });
* (async () => {
* if (await db.dataExists({ Name: 'John' })) {
* await db.delete({ Name: 'John' });
* } else {
* await db.insert({ Name: 'John', ID: 1 });
* })();
*/
class Database extends events_1.EventEmitter {
/**
* Returns the data stored in the database
*/
get _data() {
return this.data;
}
/**
* Returns the path to the file where the data is stored
*/
get path() {
return this.path;
}
/**
* Returns the default data that will be used when inserting new data
*/
get defaultData() {
return this.defaultData;
}
/**
* Creates a new instance of the Database class.
* @param {string} _path - The path to the file where the data will be stored
* @param {T} _defaultData - The default data that will be used when inserting new data
* @constructor
* @example
* const db = new Database('user-database', { Name: String, ID: Number });
* db.on('error', (err) => {
* console.log(err);
* });
* (async () => {
* if (await db.dataExists({ Name: 'John' })) {
* await db.delete({ Name: 'John' })
* } else {
* await db.insert({ Name: 'John', ID: 1 });
* })();
*/
constructor(_path, _defaultData) {
super();
this._path = _path;
this._defaultData = _defaultData;
this.data = [];
/**
* Whether an error occurred during the last operation
*/
this.errorOccurred = false;
/**
* Whether the data has been loaded from the file
*/
this.dataLoaded = false;
if (!_path.endsWith('.fastdb')) {
this._path = `${_path}.fastdb`;
}
}
/**
* @param {E} event - The name of the event to listen for
* @param {DBEvents<T>[E]} listener - The listener function that will be called when the event is emitted
* @template {keyof DBEvents<T>} E - The type of event to listen for
* @returns {this} - The instance of the Database class
* @override
*/
on(event, listener) {
return super.on(event, listener);
}
/**
* @param {E} event - The name of the event to listen for
* @param {DBEvents<T>[E]} listener - The listener function that will once be called when the event is emitted
* @template {keyof DBEvents<T>} E - The type of event to listen for
* @returns {this} - The instance of the Database class
* @override
*/
once(event, listener) {
return super.once(event, listener);
}
/**
* @param {E} event - The name of the event to emit
* @param {Parameters<DBEvents<T>>[E]} args - The arguments to pass to the listener functions
* @returns {boolean} - Whether the event was emitted successfully
* @template {keyof DBEvents<T>} E - The type of event to emit
* @override
*/
emit(event, ...args) {
return super.emit(event, ...args);
}
/**
* Loads the data from the file into the database.
* @async
* @returns {Promise<void>} - A promise that resolves when the data has been loaded
*/
async loadData() {
if (!this.dataLoaded) {
try {
const fileContent = await (0, promises_1.readFile)(this._path, 'utf-8');
this.data = JSON.parse(fileContent);
this.dataLoaded = true;
this.emit('connected', { Path: this._path });
}
catch (error) {
if (error.code === 'ENOENT') {
this.data = [];
await this.save();
}
else {
throw error;
}
}
}
}
async save() {
await (0, promises_1.writeFile)(this._path, JSON.stringify(this.data, null, 2));
}
/**
* Inserts a new data entry into the database.
* @async
* @param {Partial<T>} data - The data to insert into the database
* @returns {Promise<T | undefined>} - The data that was inserted into the database. Undefined if an error occurred
* @example
* const data = await db.insert({ Name: 'John', ID: 1 });
* console.log(data.Name); // John
* console.log(data.ID); // 1
*/
async insert(data) {
try {
await this.loadData();
if (!data) {
this.emit('error', new Error('Data to insert cannot be empty'));
return undefined;
}
const newEntry = { ...this._defaultData, ...data };
this.data.push(newEntry);
await this.save();
this.emit('newData', newEntry);
return newEntry;
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return undefined;
}
}
/**
* Gets all the data entries in the database and returns them.
* @async
* @returns {Promise<T[] | null>} - The data entries that were found. Null if an error occurred
* @example
* const data = await db.getAll();
* console.log(data[0].Name); // John
* console.log(data[0].ID); // 1
*/
async getAll() {
try {
await this.loadData();
return this.data;
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return null;
}
}
/**
* Deletes all the data entries in the database.
* @async
* @returns {Promise<boolean>} - Whether the operation was successful
* @example
* const success = await db.deleteAll();
* console.log(success); // true
* console.log(await db.getAll()); // []
*/
async deleteAll() {
try {
await this.loadData();
const originalLength = this.data.length;
this.data = [];
await this.save();
this.emit('dataDeleted', { OriginalLength: originalLength, ActualLength: 0, Completed: true });
return true;
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return false;
}
}
/**
* Gets a single data entry in the database that matches the query and returns it.
* @async
* @param {Partial<T>} query - The query to search for in the database
* @returns {Promise<T | undefined>} - The data entry that was found. Undefined if an error occurred
* @example
* const data = await db.get({ Name: 'John' });
* console.log(data.Name); // John
* console.log(data.ID); // 1
*/
async get(query) {
try {
await this.loadData();
if (!query) {
this.emit('error', new Error('Cannot search for an empty query'));
return undefined;
}
return this.data.find(entry => {
for (const key in query) {
if (query[key] !== entry[key]) {
return false;
}
}
return true;
});
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return undefined;
}
}
/**
* Deletes data entries in the database that match the query.
* @async
* @param {Partial<T>} query - The query to search for in the database
* @returns {Promise<number>} - The number of data entries that were deleted
* @example
* const count = await db.delete({ Name: 'John' });
* console.log(count); // 1
* console.log(await db.getAll()); // []
*/
async delete(query) {
try {
await this.loadData();
if (!query || Object.keys(query).length === 0) {
throw new Error('Cannot delete with an empty query');
}
const originalLength = this.data.length;
this.data = this.data.filter(entry => {
for (const key in query) {
if (query[key] !== entry[key]) {
return true;
}
}
return false;
});
const deletedCount = originalLength - this.data.length;
await this.save();
this.emit('dataDeleted', {
OriginalLength: originalLength,
ActualLength: this.data.length,
Completed: true
});
return deletedCount;
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return 0;
}
}
/**
* Determines whether data entries exist in the database that match the query.
* @async
* @param {Partial<T>} query - The query to search for in the database
* @returns {Promise<boolean>} - Whether the data entries exist in the database
* @example
* const exists = await db.dataExists({ Name: 'John' });
* console.log(exists); // true
*/
async dataExists(query) {
try {
await this.loadData();
if (!query || Object.keys(query).length === 0) {
throw new Error('Cannot check existence with an empty query');
}
return this.data.some(entry => {
for (const key in query) {
if (query[key] !== entry[key]) {
return false;
}
}
return true;
});
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return false;
}
}
/**
* Counts the number of data entries in the database that match the query.
* @async
* @param {Partial<T>} query - The query to search for in the database
* @returns {Promise<number | undefined>} - The number of data entries that were found. Undefined if an error occurred
* @example
* const count = await db.countEntries({ Name: 'John' });
* console.log(count); // 1
*/
async countEntries(query) {
if (!query || Object.keys(query).length === 0) {
throw new Error('Cannot count entries with an empty query');
}
try {
await this.loadData();
return this.data.filter(entry => {
for (const key in query) {
if (query[key] !== entry[key]) {
return false;
}
}
return true;
}).length;
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return undefined;
}
}
/**
* Schedules a specified task to run at a specified time and execute a database operation.
* @param {() => Promise<void>} task - The task to perform on the database
* @param {Date} date - When the task should be executed
* @returns {NodeJS.Timeout} - The timeout object that can be used to cancel the task
* @example
* db.scheduleTask(async () => {
* await db.insert({ Name: 'John', ID: 1 });
* }, new Date('2025-12-17T03:24:00'));
* console.log(await db.getAll()); // []
*/
scheduleTask(task, date) {
const now = new Date();
const delay = date.getTime() - now.getTime();
if (delay < 0) {
throw new Error('Scheduled time must be in the future.');
}
return setTimeout(async () => {
try {
await task();
}
catch (error) {
this.emit('error', error);
}
}, delay);
}
/**
* Finds distinct values for a specified field in the database.
* @async
* @param {K} field - The field to find distinct values for
* @template K - The type of field to find distinct values for
* @returns {Promise<Array<T[K]> | null>} - The distinct values that were found. Null if an error occurred
* @example
* const values = await db.findDistinct('Name');
* console.log(values); // ['John', 'Jane'];
*/
async findDistinct(field) {
if (!field) {
throw new Error('Field to find distinct values for cannot be empty');
}
try {
await this.loadData();
const data = await this.getAll();
if (data) {
const values = data.map(entry => entry[field]);
return Array.from(new Set(values));
}
return null;
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return null;
}
}
/**
* Filters the data in the database using a specified filter function.
* @async
* @param {(entry: T) => boolean} filterFn - The filter function to use
* @returns {Promise<T[] | undefined>} - The data entries that were found. Undefined if an error occurred
* @example
* const data = await db.filterData(entry => entry.Name === 'John');
* console.log(data[0].Name); // John
*/
async filterData(filterFn) {
if (!filterFn) {
throw new Error('Filter function cannot be empty');
}
try {
await this.loadData();
return this.data.filter(filterFn);
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return undefined;
}
}
/**
* Paginates the data in the database and returns a specified number of data entries.
* @async
* @param {number} page - The page number to retrieve
* @param {number} pageSize - The number of data entries to retrieve per page
* @returns {Promise<T[] | undefined>} - The data entries that were found. Undefined if an error occurred
* @example
* const data = await db.paginateData(1, 1);
* console.log(data[0].Name); // John
*/
async paginateData(page, pageSize) {
if (!page || !pageSize || page < 1 || pageSize < 1) {
throw new Error('Invalid parameters for pagination');
}
try {
await this.loadData();
const start = (page - 1) * pageSize;
const end = start + pageSize;
return this.data.slice(start, end);
}
catch (error) {
this.emit('error', error);
this.errorOccurred = true;
return undefined;
}
}
}
/**
* Creates a new instance of the Database class.
* @param {string} path - The path to the file where the data will be stored
* @param {T} defaultData - The default data that will be used when inserting new data
* @returns {Database<T>} - The instance of the Database class
* @template T - The type of data that the database will store
* @example
* const db = createDatabase('user-database', { Name: String, ID: Number });
* db.on('error', (err) => {
* console.log(err);
* });
* (async () => {
* if (await db.dataExists({ Name: 'John' })) {
* await db.delete({ Name: 'John' });
* } else {
* await db.insert({ Name: 'John', ID: 1 });
* })();
*/
function createDatabase(path, defaultData) {
return new Database(path, defaultData);
}
exports.createDatabase = createDatabase;
/**
* Creates a backup of the database data.
* @param {Database<T>} db - The database to create a backup of
* @param {string} backupPath - The path to save the backup file
* @template T - The type of data that the database stores
* @returns {Promise<boolean>} - Whether the backup was created successfully
* @example
* const success = await createBackup(db, 'backup.json');
* console.log(success); // true
*/
async function createBackup(db, backupPath) {
if (!db || !backupPath) {
throw new Error('Missing one or more required arguments');
}
try {
if (!backupPath.endsWith('.fastdb-backup')) {
backupPath = `${backupPath}.fastdb-backup`;
}
if (db.dataLoaded) {
const data = await db.getAll();
await (0, promises_1.writeFile)(backupPath, JSON.stringify(data, null, 2));
return true;
}
else {
throw new Error('Database data not loaded');
}
}
catch (error) {
throw error;
}
}
exports.createBackup = createBackup;