UNPKG

axiodb

Version:

The Pure JavaScript Alternative to SQLite. Embedded NoSQL database for Node.js with MongoDB-style queries, zero native dependencies, built-in InMemoryCache, and web GUI. Perfect for desktop apps, CLI tools, and embedded systems. No compilation, no platfor

355 lines 19 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Converter_helper_1 = __importDefault(require("../../Helper/Converter.helper")); const Crypto_helper_1 = require("../../Helper/Crypto.helper"); const response_helper_1 = __importDefault(require("../../Helper/response.helper")); const DocumentLoader_helper_1 = __importDefault(require("../../Helper/DocumentLoader.helper")); const FileManager_1 = __importDefault(require("../../engine/Filesystem/FileManager")); const Searcher_utils_1 = __importDefault(require("../../utility/Searcher.utils")); const SortData_utils_1 = __importDefault(require("../../utility/SortData.utils")); const crypto_1 = require("crypto"); // Validator const Create_operation_1 = __importDefault(require("./Create.operation")); const memory_operation_1 = __importDefault(require("../../Memory/memory.operation")); const Keys_1 = require("../../config/Keys/Keys"); const ReadIndex_service_1 = require("../Index/ReadIndex.service"); const LockManager_service_1 = __importDefault(require("../Transaction/LockManager.service")); class UpdateOperation { constructor(collectionName, path, baseQuery, isEncrypted = false, encryptionKey) { this.allDataWithFileName = []; this.collectionName = collectionName; this.path = path; this.baseQuery = baseQuery; this.isEncrypted = isEncrypted; this.encryptionKey = encryptionKey; this.updatedAt = new Date().toISOString(); this.sort = {}; this.Insertion = new Create_operation_1.default(this.collectionName, this.path); this.ResponseHelper = new response_helper_1.default(); this.Converter = new Converter_helper_1.default(); this.fileManager = new FileManager_1.default(); if (this.isEncrypted === true) { this.cryptoInstance = new Crypto_helper_1.CryptoHelper(this.encryptionKey); } this.allDataWithFileName = []; // To store all data with file name } /** * Updates a single document that matches the base query. * * This method performs the following operations with ACID compliance: * 1. Acquires lock on document to ensure atomicity and isolation * 2. Searches for documents matching the base query * 3. If documents are found, selects the first document (or first after sorting if sort criteria are provided) * 4. Deletes the existing document file * 5. Inserts a new file with updated data using the same document ID * 6. Releases lock * * @param newData - The new data to replace the existing document * @returns A Promise resolving to: * - Success with updated data and previous data if successful * - Error if any step fails (lock acquisition, read, update) * @throws May throw errors during file operations or data processing */ UpdateOne(newData) { return __awaiter(this, void 0, void 0, function* () { var _a; const lockManager = new LockManager_service_1.default(this.path); const operationId = (0, crypto_1.randomUUID)(); const timestamp = Date.now(); let documentId = null; try { // check if the data is an empty object or not if (Object.keys(newData).length === 0 || newData === undefined) { throw new Error("Data cannot be an empty."); } // check if the data is an object or not if (typeof newData !== "object") { throw new Error("Data must be an object."); } // STEP 1: Find the document first (before acquiring lock) let ReadResponse; // Read Response Holder if (((_a = this.baseQuery) === null || _a === void 0 ? void 0 : _a.documentId) !== undefined) { const FilePath = [ `${this.baseQuery.documentId}${Keys_1.General.DBMS_File_EXT}`, ]; ReadResponse = yield this.LoadAllBufferRawData(FilePath); } else { const fileNames = yield new ReadIndex_service_1.ReadIndex(this.path).getFileFromIndex(this.baseQuery); if (fileNames && fileNames.length > 0) { // Load File Names from Index ReadResponse = yield this.LoadAllBufferRawData(fileNames); } else { ReadResponse = yield this.LoadAllBufferRawData(); } } if (!("data" in ReadResponse)) { return this.ResponseHelper.Error("Failed to read raw data"); } const SearchedData = yield new Searcher_utils_1.default(ReadResponse.data, true).find(this.baseQuery, "data"); if (SearchedData.length === 0) { return this.ResponseHelper.Error("No data found with the specified query"); } let selectedFirstData = SearchedData[0]; // Select the first data let fileName = selectedFirstData === null || selectedFirstData === void 0 ? void 0 : selectedFirstData.fileName; // Get the file name // Sort the data if sort is provided then select the first data for deletion if (Object.keys(this.sort).length !== 0) { const Sorter = new SortData_utils_1.default(SearchedData, this.sort); const SortedData = yield Sorter.sort("data"); // Sort the data selectedFirstData = SortedData[0]; // Select the first data fileName = selectedFirstData === null || selectedFirstData === void 0 ? void 0 : selectedFirstData.fileName; // Get the file name } documentId = fileName.split(".")[0]; // STEP 2: Acquire lock on the document (ACID: Isolation) const lockResult = yield lockManager.acquireLock(documentId, operationId, timestamp); if (!("data" in lockResult)) { return this.ResponseHelper.Error("Lock acquisition failed"); } // STEP 3: Perform update operation (now safe - locked) const documentOldData = selectedFirstData.data; // Get the old data const dataForRest = Object.assign({}, documentOldData); // Get the data for rest of the fields // Update All new Fields in the old data for (const key in newData) { documentOldData[key] = newData[key]; // also change the updatedAt field documentOldData.updatedAt = this.updatedAt; } // Delete the file const deleteResponse = yield this.deleteFileUpdate(fileName); if (!("data" in deleteResponse)) { return this.ResponseHelper.Error("Failed to delete file"); } // Insert the new Data in the file const InsertResponse = yield this.insertUpdate(documentOldData, documentId); if (!("data" in InsertResponse)) { return this.ResponseHelper.Error("Failed to insert data"); } // Fire-and-forget: Invalidate cache asynchronously memory_operation_1.default.invalidateByDocument(this.path, documentId).catch(() => { }); return this.ResponseHelper.Success({ message: "Data updated successfully", newData: documentOldData, previousData: dataForRest, documentId: documentId, }); } catch (error) { console.log(error); return this.ResponseHelper.Error("Failed to update data"); } finally { // STEP 4: Always release lock (ACID: ensures no deadlock) if (documentId) { yield lockManager.releaseLock(documentId).catch(() => { }); } } }); } /** * Updates multiple documents that match the base query. * * This method performs the following operations with ACID compliance: * 1. Searches for documents matching the base query * 2. Acquires locks on ALL matching documents (ensures atomicity across all updates) * 3. Deletes the existing documents * 4. Inserts new files with updated data for each document * 5. Releases all locks * * @param newData - The new data to replace the existing documents * @returns A Promise resolving to: * - Success with updated data and previous data if successful * - Error if any step fails (rollback by releasing all acquired locks) * @throws May throw errors during file operations or data processing */ UpdateMany(newData) { return __awaiter(this, void 0, void 0, function* () { const lockManager = new LockManager_service_1.default(this.path); const operationId = (0, crypto_1.randomUUID)(); const timestamp = Date.now(); const acquiredLocks = []; try { // check if the data is an empty object or not if (Object.keys(newData).length === 0 || newData === undefined) { throw new Error("Data cannot be an empty."); } // check if the data is an object or not if (typeof newData !== "object") { throw new Error("Data must be an object."); } newData.updatedAt = new Date().toISOString(); // STEP 1: Find all matching documents let ReadResponse; const fileNames = yield new ReadIndex_service_1.ReadIndex(this.path).getFileFromIndex(this.baseQuery); if (fileNames.length > 0) { // Load File Names from Index ReadResponse = yield this.LoadAllBufferRawData(fileNames); } else { ReadResponse = yield this.LoadAllBufferRawData(); } if (!("data" in ReadResponse)) { return this.ResponseHelper.Error("Failed to read raw data"); } const SearchedData = yield new Searcher_utils_1.default(ReadResponse.data, true).find(this.baseQuery, "data"); if (SearchedData.length === 0) { return this.ResponseHelper.Error("No data found with the specified query"); } // STEP 2: Extract all document IDs const documentIds = []; for (let i = 0; i < SearchedData.length; i++) { const selectedData = SearchedData[i]; const fileName = selectedData === null || selectedData === void 0 ? void 0 : selectedData.fileName; const documentId = fileName.split(".")[0]; documentIds.push(documentId); } // STEP 3: Acquire locks on ALL documents before any modification (ACID: Atomicity + Isolation) // This prevents partial updates if one lock fails for (const docId of documentIds) { const lockResult = yield lockManager.acquireLock(docId, operationId, timestamp); if (!("data" in lockResult)) { // Lock acquisition failed - rollback by releasing acquired locks return this.ResponseHelper.Error(`Lock acquisition failed for document ${docId}`); } acquiredLocks.push(docId); } // STEP 4: All locks acquired - now perform updates safely for (let i = 0; i < SearchedData.length; i++) { let selectedData = SearchedData[i]; let fileName = selectedData === null || selectedData === void 0 ? void 0 : selectedData.fileName; const documentOldData = selectedData.data; // Sort the data if sort is provided if (Object.keys(this.sort).length !== 0) { const Sorter = new SortData_utils_1.default(SearchedData, this.sort); const SortedData = yield Sorter.sort("data"); selectedData = SortedData[i]; fileName = selectedData === null || selectedData === void 0 ? void 0 : selectedData.fileName; } const documentId = fileName.split(".")[0]; // Update All new Fields in the old data for (const key in newData) { documentOldData[key] = newData[key]; documentOldData.updatedAt = newData.updatedAt; } // Delete the file const deleteResponse = yield this.deleteFileUpdate(fileName); if (!("data" in deleteResponse)) { return this.ResponseHelper.Error(`Failed to delete file for document ${documentId}`); } // Insert the new Data in the file const InsertResponse = yield this.insertUpdate(documentOldData, documentId); if (!("data" in InsertResponse)) { return this.ResponseHelper.Error(`Failed to insert data for document ${documentId}`); } } // Fire-and-forget: Invalidate cache asynchronously memory_operation_1.default.invalidateByDocuments(this.path, documentIds).catch(() => { }); return this.ResponseHelper.Success({ message: "Data updated successfully", effectedData: SearchedData.length, documentIds: documentIds, }); } catch (error) { return this.ResponseHelper.Error("Failed to update data"); } finally { // STEP 5: Always release ALL acquired locks (ACID: ensures no deadlock) if (acquiredLocks.length > 0) { yield lockManager.releaseAllLocks(acquiredLocks).catch(() => { }); } } }); } /** * to be sorted to the query this.createdAt = new Date().toISOString(); this.updatedAt = this.createdAt; // Initially updatedAt is same as createdAt * @param {object} sort - The sort to be set. * @returns {DeleteOperation} - An instance of the DeleteOperation class. */ Sort(sort) { this.sort = sort; return this; } /** * Loads all buffer raw data from the specified directory. * * This method performs the following steps: * 1. Checks if the directory is locked. * 2. If the directory is not locked, it lists all files in the directory. * 3. Reads each file and decrypts the data if encryption is enabled. * 4. Stores the decrypted data in the `AllData` array. * 5. If the directory is locked, it unlocks the directory, reads the files, and then locks the directory again. * * @returns {Promise<SuccessInterface | ErrorInterface>} A promise that resolves to a success or error response. * * @throws {Error} Throws an error if any operation fails. */ LoadAllBufferRawData(documentIdDirectFile) { return __awaiter(this, void 0, void 0, function* () { // Use shared DocumentLoader helper (DRY - consolidates duplicated code) const result = yield DocumentLoader_helper_1.default.loadDocuments(this.path, this.encryptionKey, this.isEncrypted, documentIdDirectFile, true // Include fileName for Update operations ); // Store result in allDataWithFileName if successful if ("data" in result) { this.allDataWithFileName = result.data; } return result; }); } /** * Deletes a file from the specified path. * * This method checks if the directory is locked before attempting to delete the file. * If the directory is locked, it tries to unlock it, delete the file, and then lock it again. * * @param fileName - The name of the file to be deleted * @returns A response object indicating success or failure * Success response: { status: true, message: "File deleted successfully" } * Error response: { status: false, message: <error message> } * @private */ deleteFileUpdate(fileName) { return __awaiter(this, void 0, void 0, function* () { // Use FileManager's DeleteFileWithLock method for proper lock management return yield this.fileManager.DeleteFileWithLock(this.path, fileName); }); } /** * Inserts a document into the collection. * @param {object} data - The data to be inserted. * @returns {Promise<any>} - A promise that resolves with the response of the insertion operation. */ insertUpdate(data, ExistingdocumentId) { return __awaiter(this, void 0, void 0, function* () { // Check if data is empty or not if (!data) { throw new Error("Data cannot be empty"); } // Check if data is an object or not if (typeof data !== "object") { throw new Error("Data must be an object."); } // Encrypt the data if crypto is enabled if (this.isEncrypted && this.cryptoInstance !== undefined) { data = yield this.cryptoInstance.encrypt(this.Converter.ToString(data)); } // Save the data return yield this.Insertion.Save(data, ExistingdocumentId); }); } } exports.default = UpdateOperation; //# sourceMappingURL=Update.operation.js.map