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
JavaScript
"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