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
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 });
// 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