nope.db
Version:
A modern, simple, and async JSON database for Node.js with zero dependencies and a data-safe queue.
245 lines (244 loc) • 9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageManager = void 0;
const promises_1 = __importDefault(require("fs/promises"));
const error_js_1 = require("./error.js");
class StorageManager {
file;
spaces;
separator;
queue;
errors = {
dataNotANumber: "Existing data for this ID is not of type 'number'.",
mustBeANumber: "The provided value must be of type 'number'.",
mustBeArray: "The existing data must be of type 'array'.",
nonValidID: "Invalid ID. It cannot be empty, start/end with a separator, or contain repeated separators.",
undefinedID: "ID is undefined.",
undefinedValue: "Value is undefined.",
parseError: "Failed to parse database file. Check for corrupt JSON.",
clearConfirm: "Accidental clear prevented. Must pass { confirm: true } to clear()."
};
constructor(settings) {
this.file = settings.file;
this.spaces = settings.spaces;
this.separator = settings.separator;
this.queue = Promise.resolve();
this._enqueue(async () => {
try {
await promises_1.default.access(this.file);
}
catch (error) {
if (error?.code === 'ENOENT') {
await this._write({});
return;
}
throw new error_js_1.DatabaseError(`Unable to access database file: ${error?.message ?? error}`);
}
});
}
_enqueue(task) {
const nextTask = this.queue.catch(() => { }).then(task);
this.queue = nextTask;
return nextTask;
}
async _read() {
try {
const data = await promises_1.default.readFile(this.file, "utf-8");
return JSON.parse(data);
}
catch (error) {
if (error.code === 'ENOENT') {
await this._write({});
return {};
}
if (error instanceof SyntaxError) {
throw new error_js_1.DatabaseError(this.errors.parseError);
}
throw new error_js_1.DatabaseError(`Unable to read database file: ${error?.message ?? error}`);
}
}
async _write(data) {
await promises_1.default.writeFile(this.file, JSON.stringify(data, null, this.spaces));
}
_validateID(id) {
if (typeof id !== 'string' || !id ||
id.startsWith(this.separator) ||
id.endsWith(this.separator) ||
id.includes(this.separator + this.separator)) {
throw new error_js_1.DatabaseError(this.errors.nonValidID);
}
}
_find(data, id) {
this._validateID(id);
const parts = id.split(this.separator);
let current = data;
for (const part of parts) {
if (typeof current !== 'object' || current === null) {
return null;
}
current = current[part];
}
return current ?? null;
}
_findAndSet(data, id, value) {
this._validateID(id);
const parts = id.split(this.separator);
let current = data;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (i === parts.length - 1) {
current[part] = value;
}
else {
if (typeof current[part] !== 'object' || current[part] === null) {
current[part] = {};
}
current = current[part];
}
}
return data;
}
set(id, value) {
return this._enqueue(async () => {
if (!id)
throw new error_js_1.DatabaseError(this.errors.undefinedID);
if (value === undefined)
throw new error_js_1.DatabaseError(this.errors.undefinedValue);
let data = await this._read();
this._findAndSet(data, id, value);
await this._write(data);
return value;
});
}
get(id) {
return this._enqueue(async () => {
if (!id)
throw new error_js_1.DatabaseError(this.errors.undefinedID);
const data = await this._read();
return this._find(data, id);
});
}
add(id, value) {
return this._enqueue(async () => {
if (!id)
throw new error_js_1.DatabaseError(this.errors.undefinedID);
if (value === undefined)
throw new error_js_1.DatabaseError(this.errors.undefinedValue);
if (typeof value !== "number")
throw new error_js_1.DatabaseError(this.errors.mustBeANumber);
let data = await this._read();
const currentVal = this._find(data, id);
if (currentVal !== null && typeof currentVal !== "number") {
throw new error_js_1.DatabaseError(this.errors.dataNotANumber);
}
const newVal = (currentVal || 0) + value;
this._findAndSet(data, id, newVal);
await this._write(data);
return newVal;
});
}
subtract(id, value) {
if (typeof value !== "number")
throw new error_js_1.DatabaseError(this.errors.mustBeANumber);
return this.add(id, -value);
}
all() {
return this._enqueue(() => this._read());
}
has(id) {
return this._enqueue(async () => {
if (!id)
throw new error_js_1.DatabaseError(this.errors.undefinedID);
const data = await this._read();
return this._find(data, id) !== null;
});
}
delete(id) {
return this._enqueue(async () => {
if (!id)
throw new error_js_1.DatabaseError(this.errors.undefinedID);
let data = await this._read();
const parts = id.split(this.separator);
let current = data;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (typeof current !== 'object' || current === null) {
return false;
}
if (i === parts.length - 1) {
if (current[part] === undefined)
return false;
delete current[part];
}
else {
current = current[part];
}
}
await this._write(data);
return true;
});
}
clear(options) {
return this._enqueue(async () => {
if (options?.confirm !== true) {
throw new error_js_1.DatabaseError(this.errors.clearConfirm);
}
await this._write({});
return true;
});
}
push(id, value) {
return this._enqueue(async () => {
if (!id)
throw new error_js_1.DatabaseError(this.errors.undefinedID);
if (value === undefined)
throw new error_js_1.DatabaseError(this.errors.undefinedValue);
let data = await this._read();
let arr = this._find(data, id);
if (arr === null || arr === undefined) {
arr = [];
}
else if (!Array.isArray(arr)) {
throw new error_js_1.DatabaseError(this.errors.mustBeArray);
}
arr.push(value);
this._findAndSet(data, id, arr);
await this._write(data);
return arr;
});
}
backup(filePath) {
return this._enqueue(async () => {
if (!filePath || typeof filePath !== 'string') {
throw new error_js_1.DatabaseError("Invalid backup file path provided.");
}
if (!filePath.endsWith(".json")) {
throw new error_js_1.DatabaseError("The backup file path must end with '.json'.");
}
const data = await this._read();
await promises_1.default.writeFile(filePath, JSON.stringify(data, null, this.spaces));
return true;
});
}
loadBackup(filePath) {
return this._enqueue(async () => {
if (!filePath || typeof filePath !== 'string') {
throw new error_js_1.DatabaseError("Invalid backup file path provided.");
}
let backupData;
try {
const data = await promises_1.default.readFile(filePath, "utf-8");
backupData = JSON.parse(data);
}
catch (error) {
throw new error_js_1.DatabaseError(`Failed to read or parse backup file: ${filePath}`);
}
await this._write(backupData);
return true;
});
}
}
exports.StorageManager = StorageManager;