node-json-db
Version:
Database using JSON file as storage for Node.JS
423 lines • 16.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonDB = exports.defaultSerializers = exports.BigIntSerializer = exports.RegExpSerializer = exports.MapSerializer = exports.SetSerializer = exports.DateSerializer = exports.FileAdapter = exports.JsonAdapter = exports.DataError = exports.DatabaseError = exports.ConfigWithAdapter = exports.Config = void 0;
const Utils_1 = require("./lib/Utils");
const Errors_1 = require("./lib/Errors");
const DBParentData_1 = require("./lib/DBParentData");
const ArrayInfo_1 = require("./lib/ArrayInfo");
const Lock_1 = require("./lock/Lock");
//Export to be used by other projects that want to extend JsonDB
var JsonDBConfig_1 = require("./lib/JsonDBConfig");
Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return JsonDBConfig_1.Config; } });
Object.defineProperty(exports, "ConfigWithAdapter", { enumerable: true, get: function () { return JsonDBConfig_1.ConfigWithAdapter; } });
var Errors_2 = require("./lib/Errors");
Object.defineProperty(exports, "DatabaseError", { enumerable: true, get: function () { return Errors_2.DatabaseError; } });
Object.defineProperty(exports, "DataError", { enumerable: true, get: function () { return Errors_2.DataError; } });
var JsonAdapter_1 = require("./adapter/data/JsonAdapter");
Object.defineProperty(exports, "JsonAdapter", { enumerable: true, get: function () { return JsonAdapter_1.JsonAdapter; } });
var FileAdapter_1 = require("./adapter/file/FileAdapter");
Object.defineProperty(exports, "FileAdapter", { enumerable: true, get: function () { return FileAdapter_1.FileAdapter; } });
var Serializers_1 = require("./adapter/data/Serializers");
Object.defineProperty(exports, "DateSerializer", { enumerable: true, get: function () { return Serializers_1.DateSerializer; } });
Object.defineProperty(exports, "SetSerializer", { enumerable: true, get: function () { return Serializers_1.SetSerializer; } });
Object.defineProperty(exports, "MapSerializer", { enumerable: true, get: function () { return Serializers_1.MapSerializer; } });
Object.defineProperty(exports, "RegExpSerializer", { enumerable: true, get: function () { return Serializers_1.RegExpSerializer; } });
Object.defineProperty(exports, "BigIntSerializer", { enumerable: true, get: function () { return Serializers_1.BigIntSerializer; } });
Object.defineProperty(exports, "defaultSerializers", { enumerable: true, get: function () { return Serializers_1.defaultSerializers; } });
class JsonDB {
loaded = false;
data = {};
config;
/**
* JSONDB Constructor
* @param config Configuration for the database
*/
constructor(config) {
this.config = config;
}
/**
* Process datapath into different parts
* @param dataPath
*/
processDataPath(dataPath) {
if (dataPath === undefined || !dataPath.trim()) {
throw new Errors_1.DataError("The Data Path can't be empty", 6);
}
if (dataPath == this.config.separator) {
return [];
}
dataPath = (0, Utils_1.removeTrailingChar)(dataPath, this.config.separator);
const path = dataPath.split(this.config.separator);
path.shift();
return path;
}
async retrieveData(dataPath, create = false) {
await this.load();
const thisDb = this;
const recursiveProcessDataPath = (data, index) => {
let property = dataPath[index];
/**
* Find the wanted Data or create it.
*/
function findData(isArray = false) {
if (data.hasOwnProperty(property)) {
data = data[property];
}
else if (create) {
if (isArray) {
data[property] = [];
}
else {
data[property] = {};
}
data = data[property];
}
else {
throw new Errors_1.DataError(`Can't find dataPath: ${thisDb.config.separator}${dataPath.join(thisDb.config.separator)}. Stopped at ${property}`, 5);
}
}
const arrayInfo = ArrayInfo_1.ArrayInfo.processArray(property);
if (arrayInfo) {
property = arrayInfo.property;
findData(true);
if (!Array.isArray(data)) {
throw new Errors_1.DataError(`DataPath: ${thisDb.config.separator}${dataPath.join(thisDb.config.separator)}. ${property} is not an array.`, 11);
}
const arrayIndex = arrayInfo.getIndex(data, true);
if (!arrayInfo.append && data.hasOwnProperty(arrayIndex)) {
data = arrayInfo.getData(data);
}
else if (create) {
if (arrayInfo.append) {
data.push({});
data = data[data.length - 1];
}
else {
data[arrayIndex] = {};
data = data[arrayIndex];
}
}
else {
throw new Errors_1.DataError(`DataPath: ${thisDb.config.separator}${dataPath.join(thisDb.config.separator)}. . Can't find index ${arrayInfo.index} in array ${property}`, 10);
}
}
else {
findData();
}
if (dataPath.length == ++index) {
// check data
return data;
}
return recursiveProcessDataPath(data, index);
};
if (dataPath.length === 0) {
return this.data;
}
return recursiveProcessDataPath(this.data, 0);
}
async getParentData(dataPath, create) {
const path = this.processDataPath(dataPath);
const last = path.pop();
return new DBParentData_1.DBParentData(await this.retrieveData(path, create), this, dataPath, last);
}
/**
* Get the wanted data
* @param dataPath path of the data to retrieve
* @returns {Promise<any>}
*/
getData(dataPath) {
return (0, Lock_1.readLockAsync)(async () => {
const path = this.processDataPath(dataPath);
return this.retrieveData(path, false);
});
}
/**
* Same as getData only here it's directly typed to your object
* @param dataPath path of the data to retrieve
* @returns {Promise}
*/
getObject(dataPath) {
return this.getData(dataPath);
}
/**
* Same as getData but with your own object type and a possible default value when we can't find the data path
* @param dataPath path of the data to retrieve
* @param defaultValue value to use when the dataPath doesn't lead to data
* @returns {Promise}
* @throws {DataError}
*/
async getObjectDefault(dataPath, defaultValue) {
try {
return await this.getData(dataPath);
}
catch (e) {
if (!(e instanceof Errors_1.DataError)) {
throw e;
}
if (e.id != 5) {
throw e;
}
return defaultValue;
}
}
/**
* Check for existing datapath
* @param dataPath
* @returns {Promise<boolean>}
*/
async exists(dataPath) {
try {
await this.getData(dataPath);
return true;
}
catch (e) {
if (e instanceof Errors_1.DataError) {
return false;
}
throw e;
}
}
/**
* Returns the number of element which constitutes the array
* @param dataPath
* @returns {Promise<number>}
* @throws {DataError}
*/
async count(dataPath) {
const result = await this.getData(dataPath);
if (!Array.isArray(result)) {
throw new Errors_1.DataError(`DataPath: ${dataPath} is not an array.`, 11);
}
const path = this.processDataPath(dataPath);
const data = await this.retrieveData(path, false);
return data.length;
}
/**
* Returns the index of the object that meets the criteria submitted. Returns -1, if no match is found.
* @param dataPath base dataPath from where to start searching
* @param searchValue value to look for in the dataPath
* @param propertyName name of the property to look for searchValue
* @returns {Promise<number>}
*/
async getIndex(dataPath, searchValue, propertyName = 'id') {
const data = await this.getArrayData(dataPath);
return data
.map(function (element) {
return element[propertyName];
})
.indexOf(searchValue);
}
/**
* Return the index of the value inside the array. Returns -1, if no match is found.
* @param dataPath base dataPath from where to start searching
* @param searchValue value to look for in the dataPath
* @returns {Promise<number>}
*/
async getIndexValue(dataPath, searchValue) {
return (await this.getArrayData(dataPath)).indexOf(searchValue);
}
async getArrayData(dataPath) {
const result = await this.getData(dataPath);
if (!Array.isArray(result)) {
throw new Errors_1.DataError(`DataPath: ${dataPath} is not an array.`, 11);
}
const path = this.processDataPath(dataPath);
return this.retrieveData(path, false);
}
/**
* Find all specific entry in an array/object
* @param rootPath base dataPath from where to start searching
* @param callback method to filter the result and find the wanted entry. Receive the entry and it's index.
* @returns {Promise}
* @throws {DataError}
*/
async filter(rootPath, callback) {
const result = await this.getData(rootPath);
if (Array.isArray(result)) {
return result.filter(callback);
}
if (result instanceof Object) {
const entries = Object.entries(result);
const found = entries.filter((entry) => {
return callback(entry[1], entry[0]);
});
if (!found || found.length < 1) {
return undefined;
}
return found.map((entry) => {
return entry[1];
});
}
throw new Errors_1.DataError('The entry at the path (' +
rootPath +
') needs to be either an Object or an Array', 12);
}
/**
* Find a specific entry in an array/object
* @param rootPath base dataPath from where to start searching
* @param callback method to filter the result and find the wanted entry. Receive the entry and it's index.
* @returns {Promise}
* @throws {DataError}
*/
async find(rootPath, callback) {
const result = await this.getData(rootPath);
if (Array.isArray(result)) {
return result.find(callback);
}
if (result instanceof Object) {
const entries = Object.entries(result);
const found = entries.find((entry) => {
return callback(entry[1], entry[0]);
});
if (!found || found.length < 2) {
return undefined;
}
return found[1];
}
throw new Errors_1.DataError('The entry at the path (' +
rootPath +
') needs to be either an Object or an Array', 12);
}
/**
* Pushing data into the database
* @param dataPath path leading to the data
* @param data data to push
* @param override overriding or not the data, if not, it will merge them
* @returns {Promise<void>}
* @throws {DataError}
*/
async push(dataPath, data, override = true) {
return (0, Lock_1.writeLockAsync)(async () => {
const dbData = await this.getParentData(dataPath, true);
// if (!dbData) {
// throw new Error('Data not found')
// }
let toSet = data;
if (!override) {
if (Array.isArray(data)) {
let storedData = dbData.getData();
if (storedData === undefined) {
storedData = [];
}
else if (!Array.isArray(storedData)) {
throw new Errors_1.DataError("Can't merge another type of data with an Array", 3);
}
toSet = storedData.concat(data);
}
else if (data === Object(data)) {
if (Array.isArray(dbData.getData())) {
throw new Errors_1.DataError("Can't merge an Array with an Object", 4);
}
toSet = (0, Utils_1.merge)(dbData.getData(), data);
}
}
dbData.setData(toSet);
if (this.config.saveOnPush) {
await this.save();
}
});
}
/**
* Delete the data
* @param dataPath path leading to the data
*/
async delete(dataPath) {
await (0, Lock_1.writeLockAsync)(async () => {
const dbData = await this.getParentData(dataPath, true);
// if (!dbData) {
// return
// }
dbData.delete();
if (this.config.saveOnPush) {
await this.save();
}
});
}
/**
* Only use this if you know what you're doing.
* It reset the full data of the database.
* @param data
*/
resetData(data) {
this.data = data;
}
/**
* Reload the database from the file
*/
async reload() {
this.loaded = false;
await this.load();
}
/**
* Manually load the database
* It is automatically called when the first getData is done
* @return {Promise<void>}
* @throws {DatabaseError}
*/
async load() {
if (this.loaded) {
return;
}
try {
this.data = await this.config.adapter.readAsync();
this.loaded = true;
}
catch (err) {
throw new Errors_1.DatabaseError("Can't Load Database", 1, err);
}
}
/**
* Manually save the database
* By default you can't save the database if it's not loaded
* @param force force the save of the database
* @return {Promise<void>}
* @throws {DatabaseError}
*/
async save(force) {
force = force || false;
if (!force && !this.loaded) {
throw new Errors_1.DatabaseError("DataBase not loaded. Can't write", 7);
}
try {
await this.config.adapter.writeAsync(this.data);
}
catch (err) {
throw new Errors_1.DatabaseError("Can't save the database", 2, err);
}
}
/**
* Convert a router style path to a normal path
* By default propertyName to search is "id"
* @param path router based path to a correct base path
* @param propertyName name of the property to look for searchValue
*/
async fromPath(path, propertyName = 'id') {
const [, ...pathToQuery] = path.split("/");
const pathObject = pathToQuery.reduce((prev, curr, indexPath) => {
const isKey = indexPath % 2 === 0;
if (isKey) {
prev[`${curr}`] = '';
}
else {
const keys = Object.keys(prev);
prev[`${keys[keys.length - 1]}`] = `${curr}`;
}
return prev;
}, {});
let normalPath = [];
for await (const pathKey of Object.keys(pathObject)) {
normalPath.push(`/${pathKey}`);
const pathValue = pathObject[pathKey];
try {
const pathIndex = await this.getIndex(normalPath.join(""), pathValue, propertyName);
normalPath.push(`[${pathIndex}]`);
}
catch (error) {
throw new Errors_1.DataError(`DataPath: ${normalPath.join("")}/${pathValue} not found.`, 13, error);
}
}
return normalPath.join("");
}
}
exports.JsonDB = JsonDB;
//# sourceMappingURL=JsonDB.js.map