UNPKG

node-json-db

Version:

Database using JSON file as storage for Node.JS

416 lines 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonDB = 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; } }); 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