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

445 lines 21 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 }); // Import All helpers const memory_operation_1 = __importDefault(require("../../Memory/memory.operation")); 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 PathSanitizer_helper_1 = __importDefault(require("../../Helper/PathSanitizer.helper")); const DocumentLoader_helper_1 = __importDefault(require("../../Helper/DocumentLoader.helper")); // Import All Utility const Keys_1 = require("../../config/Keys/Keys"); const Searcher_utils_1 = __importDefault(require("../../utility/Searcher.utils")); const SortData_utils_1 = __importDefault(require("../../utility/SortData.utils")); const ReadIndex_service_1 = require("../Index/ReadIndex.service"); /** * Class representing a read operation. */ class Reader { /** * Creates an instance of Read. * @param {string} collectionName - The name of the collection. * @param {string} path - The data to be read. * @param {object} baseQuery - The base query to be used. * @param {boolean} isEncrypted - The encryption status. * @param {string} encryptionKey - The encryption key. */ constructor(collectionName, path, baseQuery, isEncrypted = false, encryptionKey) { this.collectionName = collectionName; this.path = path; this.limit = 10; this.skip = 0; this.FindOneStatus = false; // Default value for FindOneStatus this.isEncrypted = isEncrypted; this.sort = {}; this.project = {}; this.baseQuery = baseQuery; this.Converter = new Converter_helper_1.default(); this.encryptionKey = encryptionKey; this.ResponseHelper = new response_helper_1.default(); this.totalCount = false; this.AllData = []; if (this.isEncrypted === true) { this.cryptoInstance = new Crypto_helper_1.CryptoHelper(this.encryptionKey); } } /** * Generates a comprehensive cache key including collection context * Prevents cache collisions between different collections with same query * * Format: {collectionPath}::{query}::{limit}::{skip}::{sort} * * @returns Cache key string * @private */ generateCacheKey() { var _a, _b, _c, _d; const components = [ this.path, // Collection path prevents cross-collection collisions this.Converter.ToString(this.baseQuery), (_b = (_a = this.limit) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'all', (_d = (_c = this.skip) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : '0', Object.keys(this.sort).length > 0 ? this.Converter.ToString(this.sort) : 'nosort' ]; return components.join('::'); } /** * Reads the data from a file. * @returns {Promise<any>} A promise that resolves with the response of the read operation. */ exec() { return __awaiter(this, void 0, void 0, function* () { var _a; try { let SearchedData = []; // Generate cache key with collection context (fixes cache collision bug) const cacheKey = this.generateCacheKey(); // Check if result is in cache const responseFromCache = yield memory_operation_1.default.getCache(cacheKey); if (responseFromCache !== false) { SearchedData = responseFromCache; return this.applySortAndReturn(SearchedData); } // Direct documentId lookup - fastest path if (((_a = this.baseQuery) === null || _a === void 0 ? void 0 : _a.documentId) !== undefined) { // Sanitize document IDs to prevent directory traversal attacks const sanitizeDocId = (id) => `${PathSanitizer_helper_1.default.sanitizePathComponent(id)}${Keys_1.General.DBMS_File_EXT}`; const FilePath = Array.isArray(this.baseQuery.documentId) ? this.baseQuery.documentId.map(sanitizeDocId) : [sanitizeDocId(this.baseQuery.documentId)]; const ReadResponse = yield this.LoadAllBufferRawData(FilePath); if ("data" in ReadResponse) { // Fire-and-forget: Cache asynchronously memory_operation_1.default.setCache(cacheKey, ReadResponse.data).catch(() => { }); return this.ApplySkipAndLimit(ReadResponse.data); } return this.ResponseHelper.Error("Failed to read document by ID"); } // Try index-based lookup first const indexReader = new ReadIndex_service_1.ReadIndex(this.path); let indexedFileNames = []; // Check if query can use index optimization const queryKeys = Object.keys(this.baseQuery); if (queryKeys.length === 1) { const fieldName = queryKeys[0]; const fieldValue = this.baseQuery[fieldName]; if (typeof fieldValue === 'object' && fieldValue !== null) { // OPTIMIZED: Use $in-aware index lookup (O(K) vs O(N)) if ('$in' in fieldValue) { indexedFileNames = yield indexReader.getFilesForInOperator(fieldName, fieldValue.$in); } // OPTIMIZED: Use prefix index lookup for regex patterns like /^prefix/ else if ('$regex' in fieldValue) { const prefixInfo = this.detectPrefixPattern(fieldValue.$regex, fieldValue.$options); if (prefixInfo.isPrefix && prefixInfo.prefix) { indexedFileNames = yield indexReader.getFilesForPrefixQuery(fieldName, prefixInfo.prefix, prefixInfo.caseInsensitive); } else { // Non-prefix regex - use standard lookup (will likely fall back to full scan) indexedFileNames = yield indexReader.getFileFromIndex(this.baseQuery); } } // Other operators - use standard lookup else { indexedFileNames = yield indexReader.getFileFromIndex(this.baseQuery); } } else { // Standard exact match indexedFileNames = yield indexReader.getFileFromIndex(this.baseQuery); } } else { // Multiple fields or no fields - use standard index lookup indexedFileNames = yield indexReader.getFileFromIndex(this.baseQuery); } let ReadResponse; let usedIndex = false; if (indexedFileNames && indexedFileNames.length > 0) { // Index hit - load only indexed files (much faster) ReadResponse = yield this.LoadAllBufferRawData(indexedFileNames); usedIndex = true; } else { // No index match - full collection scan required ReadResponse = yield this.LoadAllBufferRawData(); } if (!("data" in ReadResponse)) { return this.ResponseHelper.Error("Failed to read data"); } // If no query filters, return all data if (Object.keys(this.baseQuery).length === 0) { // Fire-and-forget: Cache asynchronously memory_operation_1.default.setCache(cacheKey, ReadResponse.data).catch(() => { }); return this.applySortAndReturn(ReadResponse.data); } // If we used index for exact match (single field, no operators), data is already filtered if (usedIndex && this.isExactIndexMatch()) { SearchedData = ReadResponse.data; } else { // Apply searcher for complex queries or partial index matches const searcher = new Searcher_utils_1.default(ReadResponse.data); SearchedData = yield searcher.find(this.baseQuery); } // Fire-and-forget: Cache asynchronously memory_operation_1.default.setCache(cacheKey, SearchedData).catch(() => { }); return this.applySortAndReturn(SearchedData); } catch (error) { return this.ResponseHelper.Error(error); } }); } /** * Checks if the query is an exact match on a single indexed field (no operators) */ isExactIndexMatch() { const queryKeys = Object.keys(this.baseQuery); if (queryKeys.length !== 1) return false; const value = this.baseQuery[queryKeys[0]]; // Exact match if value is primitive (not an operator object) return typeof value !== 'object' || value === null; } /** * Detects if a regex pattern is a simple prefix match (e.g., /^John/, /^admin@/) * and extracts the prefix for index optimization * * @param regex - The regex pattern (RegExp object or string) * @param options - Optional regex flags (e.g., 'i' for case-insensitive) * @returns Object with isPrefix flag, prefix string, and case-insensitive flag * * @example * detectPrefixPattern(/^John/, 'i') // { isPrefix: true, prefix: 'John', caseInsensitive: true } * detectPrefixPattern(/John/) // { isPrefix: false } * detectPrefixPattern(/^test[0-9]+/) // { isPrefix: false } - complex pattern */ detectPrefixPattern(regex, options) { const regexStr = regex instanceof RegExp ? regex.source : String(regex); const flags = regex instanceof RegExp ? regex.flags : (options || ''); // Match simple prefix patterns: ^abc, ^test, ^John (alphanumeric, underscore, dash, @, .) // Excludes complex patterns with [], *, +, {}, etc. const prefixMatch = regexStr.match(/^\^([a-zA-Z0-9_\-@.]+)$/); if (prefixMatch) { return { isPrefix: true, prefix: prefixMatch[1], caseInsensitive: flags.includes('i') }; } return { isPrefix: false, caseInsensitive: false }; } /** * Applies sorting if needed and returns data with skip/limit */ applySortAndReturn(data) { return __awaiter(this, void 0, void 0, function* () { if (Object.keys(this.sort).length === 0) { return this.ApplySkipAndLimit(data); } const Sorter = new SortData_utils_1.default(data, this.sort); const SortedData = yield Sorter.sort(); return this.ApplySkipAndLimit(SortedData); }); } /** * set limit to the query * @param {number} limit - The limit to be set. * @returns {Reader} - An instance of the Reader class. */ Limit(limit) { // Check if limit is a number or not if (typeof limit !== "number") { throw new Error("Limit should be a number"); } this.limit = limit; return this; } /** * to be skipped to the query * @param {number} skip - The skip to be set. * @returns {Reader} - An instance of the Reader class. */ Skip(skip) { // Check if skip is a number or not if (typeof skip !== "number") { throw new Error("Skip should be a number"); } this.skip = skip; return this; } /** * to be sorted to the query * @param {object} sort - The sort to be set. * @returns {Reader} - An instance of the Reader class. */ Sort(sort) { // check if sort is an object or not if (typeof sort !== "object") { throw new Error("Sort should be an object"); } this.sort = sort; return this; } /** * Sets whether to include the total count of matching documents in the result. * * @param count - Boolean flag indicating whether to include the total count * @returns The Reader instance for method chaining */ setCount(count) { this.totalCount = count; return this; } findOne(status = false) { this.FindOneStatus = status; return this; } setProject(project) { // check if project is an object or not if (typeof project !== "object") { throw new Error("Project should be an object"); } this.project = project; 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, false // Don't include fileName for Reader ); // Store result in AllData if successful if ("data" in result) { this.AllData = result.data; } return result; }); } /** * Applies skip and limit to the provided data array. * * This method checks if both `limit` and `skip` are defined. If they are, * it slices the `FinalData` array according to the `skip` and `limit` values * and returns the sliced data. If either `limit` or `skip` is not defined, * it returns the original `FinalData` array. * * @param {any[]} FinalData - The array of data to apply skip and limit to. * @returns {Promise<SuccessInterface | ErrorInterface>} - A promise that resolves to a success interface containing the sliced data or the original data. */ ApplySkipAndLimit(FinalData) { return __awaiter(this, void 0, void 0, function* () { // Check if FindOneStatus is true if (this.FindOneStatus === true) { // If FindOneStatus is true then return the first document if (FinalData.length > 0) { if (Object.keys(this.project).length !== 0) { const projectionresponse = yield this.ApplyProjection([FinalData[0]]); if ("data" in projectionresponse) { return this.ResponseHelper.Success({ documents: projectionresponse.data.documents[0], }); } } return this.ResponseHelper.Success({ documents: FinalData[0], }); } else { return this.ResponseHelper.Error("No documents found"); } } // Check if limit is passed or not if (this.limit !== undefined && this.skip !== undefined) { // Apply Skip and Limit const limitedAndSkippedData = FinalData.slice(this.skip, this.skip + this.limit); if (this.totalCount) { // Apply Projectd if total count is true if (Object.keys(this.project).length !== 0) { const projectionresponse = yield this.ApplyProjection(limitedAndSkippedData); if ("data" in projectionresponse) { return this.ResponseHelper.Success({ documents: projectionresponse.data.documents, totalDocuments: FinalData.length, }); } } return this.ResponseHelper.Success({ documents: limitedAndSkippedData, totalDocuments: limitedAndSkippedData.length, }); } else { if (Object.keys(this.project).length !== 0) { const projectionresponse = yield this.ApplyProjection(limitedAndSkippedData); if ("data" in projectionresponse) { return this.ResponseHelper.Success({ documents: projectionresponse.data.documents, }); } } return this.ResponseHelper.Success({ documents: limitedAndSkippedData, }); } } return this.ResponseHelper.Success({ documents: FinalData, }); }); } // Apply Projection ApplyProjection(FinalData) { return __awaiter(this, void 0, void 0, function* () { // Special keys const SpecialKeys = ["documentId"]; // Apply Project if (Object.keys(this.project).length !== 0) { const projectedData = FinalData.map((data) => { const projectedObject = {}; const keys = Object.keys(this.project); const hasInclude = keys.some((key) => this.project[key] === 1); const hasExclude = keys.every((key) => this.project[key] === 0); if (hasInclude) { for (const [key, value] of Object.entries(this.project)) { if (value === 1) { projectedObject[key] = key in data ? data[key] : null; } } } else if (hasExclude) { for (const key in data) { if (!(key in this.project)) { projectedObject[key] = data[key]; } } } else { throw new Error("Invalid projection: mixing inclusion and exclusion is not allowed."); } // Always include documentId (and optionally updatedAt) SpecialKeys.forEach((key) => { if (key in data) { projectedObject[key] = data[key]; } }); return projectedObject; }); return this.ResponseHelper.Success({ documents: projectedData, }); } return this.ResponseHelper.Success({ documents: FinalData, }); }); } } exports.default = Reader; //# sourceMappingURL=Reader.operation.js.map