UNPKG

docudb

Version:

Document-based NoSQL database for NodeJS

1,058 lines 43 kB
/** * Main database module * Integrates all components and provides the CRUD interface */ import fs from 'node:fs'; import path from 'node:path'; import { promisify } from 'node:util'; import crypto from 'node:crypto'; import FileStorage from '../storage/fileStorage.js'; import IndexManager from '../index/indexManager.js'; import Query from '../query/query.js'; import { MCO_ERROR, DocuDBError } from '../errors/errors.js'; import { fileExists } from '../utils/fileUtils.js'; import deepCopy from '../utils/deepCopy.js'; import { isValidID } from '../utils/uuidUtils.js'; import { validatePath } from '../utils/pathValidator.js'; const mkdirPromise = promisify(fs.mkdir); const readFilePromise = promisify(fs.readFile); const writeFilePromise = promisify(fs.writeFile); class Database { /** Database name */ name; /** Directory to store data */ dataDir; /** Collections in this database */ collections; /** Storage options */ storageOptions; /** ID generation type */ idType; /** File storage instance */ storage; /** Index manager instance */ indexManager; /** Database Initialized */ _initialized; /** * Creates a new database instance * @param options - Configuration options */ constructor(options = {}) { this._initialized = false; this.name = options.name ?? 'docudb'; this.collections = {}; // ID generation options this.idType = options.idType ?? 'mongo'; // 'mongo' or 'uuid' this.dataDir = path.join(options.dataDir ?? process.cwd(), 'data'); const validate = validatePath(this.name, this.dataDir); if (validate.safePath === null) { throw new DocuDBError(`Invalid database name: ${validate.error ?? ''}`, MCO_ERROR.DATABASE.INVALID_NAME); } this.dataDir = validate.safePath; // Storage options this.storageOptions = { dataDir: this.dataDir, chunkSize: options.chunkSize ?? 1024 * 1024, // 1MB default compression: options.compression !== false }; this.storage = new FileStorage(this.storageOptions); this.indexManager = new IndexManager({ dataDir: this.dataDir }); } /** * Initializes the database * @returns {Promise<void>} */ async initialize() { try { // Create data directory if it doesn't exist if (!(await fileExists(this.dataDir))) { try { await mkdirPromise(this.dataDir, { recursive: true }); } catch (dirError) { throw new DocuDBError(`Error creating data directory: ${dirError.message}`, MCO_ERROR.DATABASE.INIT_ERROR, { originalError: dirError }); } } // Initialize storage await this.storage.initialize(); // Load existing collections metadata await this._loadCollections(); this._initialized = true; } catch (error) { throw new DocuDBError(`Error initializing database: ${error.message}`, MCO_ERROR.DATABASE.INIT_ERROR, { originalError: error }); } } /** * Gets a collection * @param {string} collectionName - Collection name * @param {CollectionOptions} options - Collection options * @returns {Collection} - Collection instance */ collection(collectionName, options = { idType: this.idType }) { if (!this._initialized) { throw new DocuDBError('Database not initialized', MCO_ERROR.DATABASE.NOT_INITIALIZED); } if (typeof collectionName !== 'string' || collectionName === '') { throw new DocuDBError('Collection name must be a valid string', MCO_ERROR.COLLECTION.INVALID_NAME); } this.collections[collectionName] = new Collection(collectionName, this.storage, this.indexManager, options); return this.collections[collectionName]; } /** * Drops a collection * @param {string} collectionName - Name of collection to drop * @returns {Promise<boolean>} - true if successfully dropped */ async dropCollection(collectionName) { try { if (!this._initialized) { throw new DocuDBError('Database not initialized', MCO_ERROR.DATABASE.NOT_INITIALIZED); } if (typeof this.collections[collectionName] !== 'object') { return false; } try { // Delete all documents and metadata await this.collections[collectionName].drop(); } catch (dropError) { // Ignore errors if directory doesn't exist if (!dropError.message.includes('ENOENT')) { throw dropError; } } // Delete collection directory const collectionDir = path.join(this.dataDir, collectionName); if (await fileExists(collectionDir)) { await fs.promises.rm(collectionDir, { recursive: true, force: true }); } // Remove from collections list delete this.collections[collectionName]; return true; } catch (error) { throw new DocuDBError(`Error dropping collection: ${error.message}`, MCO_ERROR.COLLECTION.DROP_ERROR, { collectionName, originalError: error }); } } /** * Lists all collections * @returns {Promise<string[]>} - Collection names */ async listCollections() { try { if (!this._initialized) { throw new DocuDBError('Database not initialized', MCO_ERROR.DATABASE.NOT_INITIALIZED); } return Object.keys(this.collections); } catch (error) { throw new DocuDBError(`Error listing collections: ${error.message}`, MCO_ERROR.DATABASE.COLLECTION_ERROR, { originalError: error }); } } /** * Loads existing collections * @private */ async _loadCollections() { try { // Read directories in data directory const items = await promisify(fs.readdir)(this.dataDir, { withFileTypes: true }); for (const item of items) { if (item.isDirectory() && !item.name.startsWith('_')) { const collectionName = item.name; // Create collection instance this.collections[collectionName] = new Collection(collectionName, this.storage, this.indexManager); // Initialize collection await this.collections[collectionName].initialize(); } } } catch (error) { throw new DocuDBError(`Error loading collections: ${error.message}`, MCO_ERROR.DATABASE.LOAD_ERROR, { originalError: error }); } } } export class Collection { name; storage; indexManager; options; schema; documents; metadataPath; metadata; /** * @param {string} name - Collection name * @param {FileStorage} storage - Storage instance * @param {IndexManager} indexManager - Index manager instance * @param {Object} options - Additional options */ constructor(name, storage, indexManager, options = { idType: 'mongo' }) { this.name = name; this.storage = storage; this.indexManager = indexManager; this.options = options; this.schema = options.schema != null ? options.schema : null; this.documents = {}; // Document cache const validate = validatePath(this.name, storage.dataDir); if (validate.safePath === null) { throw new DocuDBError(`Invalid collection name: ${validate.error ?? ''}`, MCO_ERROR.COLLECTION.INVALID_NAME); } this.metadataPath = path.join(storage.dataDir, name, '_metadata.json'); this.metadata = { count: 0, indices: [], created: new Date(), updated: new Date(), documentOrder: [] }; } /** * Initializes the collection * @returns {Promise<void>} */ async initialize() { try { // Create collection directory if it doesn't exist await this.storage._ensureCollectionDir(this.name); // Load metadata if exists await this._loadMetadata(); // Initialize index manager await this.indexManager.initialize(this.name); // Create indices defined in metadata for (const indexDef of this.metadata.indices) { await this.indexManager.createIndex(this.name, indexDef.field ?? '', indexDef.options); } } catch (error) { throw new DocuDBError(`Error initializing collection: ${error.message}`, MCO_ERROR.COLLECTION.METADATA_ERROR, { collectionName: this.name, originalError: error }); } } /** * Inserts a document into the collection * @param {DocumentStructure} doc - Document to insert * @returns {Promise<Document>} - Inserted document with ID */ async insertOne(doc) { try { // Validate schema if exists let validatedDoc = doc; if (this.schema != null) { validatedDoc = this.schema.validate(doc); } else { // If no schema, just check if it's an object if (typeof doc !== 'object') { throw new DocuDBError('Document must be an object', MCO_ERROR.DOCUMENT.INVALID_DOCUMENT); } } // Generate ID if it doesn't exist if (validatedDoc._id === undefined) { validatedDoc._id = this._generateId(); } else { // Check if we have a schema with custom ID validation let skipDefaultIdValidation = false; if (this.schema?.definition?._id?.validate?.pattern !== undefined) { // If we have a schema with pattern validation for _id, we'll let the schema handle it // The schema validation has already run at this point skipDefaultIdValidation = true; } // Validate ID format if provided and not using custom schema validation if (!skipDefaultIdValidation) { if (!isValidID(validatedDoc._id)) { throw new DocuDBError('Invalid document ID format. Must be a valid MongoDB ID or UUID v4', MCO_ERROR.DOCUMENT.INVALID_ID, { id: validatedDoc._id }); } } } const docId = validatedDoc._id; // Update indices await this.indexManager.updateIndex(this.name, docId, validatedDoc); // Save document const chunkPaths = await this.storage.saveData(path.join(this.name, docId), validatedDoc); // Update metadata this.documents[docId] = { _id: docId, data: validatedDoc, chunkPaths }; this.metadata.count++; this.metadata.updated = new Date(); // Add document ID to the order array if (this.metadata.documentOrder === undefined) { this.metadata.documentOrder = []; } this.metadata.documentOrder.push(docId); await this._saveMetadata(); return validatedDoc; } catch (error) { throw new DocuDBError(`Error inserting document: ${error.message}`, MCO_ERROR.DOCUMENT.INSERT_ERROR, { collectionName: this.name, originalError: error }); } } /** * Inserts multiple documents into the collection * @param {Object[]} docs - Documents to insert * @returns {Promise<Object[]>} - Inserted documents with IDs */ async insertMany(docs) { try { if (!Array.isArray(docs)) { throw new DocuDBError('Expected an array of documents', MCO_ERROR.DOCUMENT.INVALID_DOCUMENT); } const results = []; for (const doc of docs) { const result = await this.insertOne(doc); results.push(result); } return results; } catch (error) { throw new DocuDBError(`Error inserting documents: ${error.message}`, MCO_ERROR.DOCUMENT.INSERT_ERROR, { collectionName: this.name, originalError: error }); } } /** * Finds a document by its ID * @param {string} id - Document ID * @returns {Promise<Object|null>} - Found document or null */ async findById(id) { try { // Check if we have a schema with custom ID validation let skipDefaultIdValidation = false; if (this.schema?.definition?._id?.validate?.pattern !== undefined) { // If we have a schema with pattern validation for _id, we'll skip the default validation skipDefaultIdValidation = true; } // Validate ID format if not using custom schema validation if (!skipDefaultIdValidation) { if (typeof id !== 'string' || !isValidID(id)) { throw new DocuDBError('Invalid document ID format. Must be a valid MongoDB ID or UUID v4', MCO_ERROR.DOCUMENT.INVALID_ID, { id }); } } else if (typeof id !== 'string') { throw new DocuDBError('Invalid document ID format', MCO_ERROR.DOCUMENT.INVALID_ID, { id }); } // Check if in cache if (this.documents[id]?.data !== undefined) { return this.documents[id].data; } // Build chunks path const docDir = path.join(this.name, id); const docDirPath = path.join(this.storage.dataDir, docDir); // Check if exists if (!(await fileExists(docDirPath))) { return null; } // Read chunk files const files = await promisify(fs.readdir)(docDirPath); const chunkPaths = files .filter(f => f.startsWith('chunk_')) .map(f => path.join(docDirPath, f)) .sort((a, b) => { const numA = parseInt(a.match(/chunk_(\d+)/)?.[1] ?? '0'); const numB = parseInt(b.match(/chunk_(\d+)/)?.[1] ?? '0'); return numA - numB; }); if (chunkPaths.length === 0) { return null; } // Read data const doc = await this.storage.readData(chunkPaths); // Update cache this.documents[id] = { _id: id, data: doc, chunkPaths }; return doc; } catch (error) { throw new DocuDBError(`Error finding document: ${error.message}`, MCO_ERROR.DOCUMENT.NOT_FOUND, { collectionName: this.name, id, originalError: error }); } } /** * Finds documents matching criteria * @param {QueryCriteria} criteria - Search criteria * @returns {Promise<DocumentStructure[]>} - Found documents */ async find(criteria = {}) { try { const query = criteria instanceof Query ? criteria : new Query(criteria); // Try to use indices to optimize search const results = await this._findWithOptimization(query); if (results !== null) { return results; } // If optimization not possible, load all documents and filter const allDocs = await this._loadAllDocuments(); return query.execute(allDocs); } catch (error) { throw new DocuDBError(`Error in query: ${error.message}`, MCO_ERROR.DOCUMENT.QUERY_ERROR, { collectionName: this.name, originalError: error }); } } /** * Finds one document matching criteria * @param {QueryCriteria} criteria - Search criteria * @returns {Promise<Object|null>} - Found document or null */ async findOne(criteria = {}) { try { const results = await this.find(criteria); return results.length > 0 ? results[0] : null; } catch (error) { throw new DocuDBError(`Error in query: ${error.message}`, MCO_ERROR.DOCUMENT.QUERY_ERROR, { collectionName: this.name, originalError: error }); } } /** * Updates a document by its ID * @param {string} id - Document ID * @param {Object} update - Changes to apply * @returns {Promise<Document|null>} - Updated document or null */ async updateById(id, update) { try { // Check if we have a schema with custom ID validation let skipDefaultIdValidation = false; if (this.schema?.definition?._id?.validate?.pattern !== undefined) { // If we have a schema with pattern validation for _id, we'll skip the default validation skipDefaultIdValidation = true; } // Validate ID format if not using custom schema validation if (!skipDefaultIdValidation) { if (typeof id !== 'string' || !isValidID(id)) { throw new DocuDBError('Invalid document ID format. Must be a valid MongoDB ID or UUID v4', MCO_ERROR.DOCUMENT.INVALID_ID, { id }); } } else if (typeof id !== 'string') { throw new DocuDBError('Invalid document ID format', MCO_ERROR.DOCUMENT.INVALID_ID, { id }); } // Find document const doc = await this.findById(id); if (doc == null) { return null; } // Validate update operators if (typeof update === 'object') { const validOperators = [ '$set', '$unset', '$inc', '$push', '$pull', '$addToSet' ]; const operators = Object.keys(update).filter(key => key.startsWith('$')); for (const op of operators) { if (!validOperators.includes(op)) { throw new DocuDBError(`Invalid update operator: ${op}`, MCO_ERROR.DOCUMENT.UPDATE_ERROR); } } } // Apply updates const updatedDoc = this._applyUpdate(doc, update); // Validate schema if exists if (this.schema != null) { this.schema.validate(updatedDoc); } // Implement locking mechanism for concurrent operations const lockKey = `${this.name}:${id}:lock`; // Create global object for locks if it doesn't exist if (!('_documentLocks' in global)) { ; global._documentLocks = {}; } // Wait if document is locked (with retry) let retries = 0; const maxRetries = 10; const retryDelay = 50; // ms while (global._documentLocks[lockKey] !== undefined && retries < maxRetries) { await new Promise(resolve => setTimeout(resolve, retryDelay * (1 + Math.random()))); retries++; } // If still locked after several attempts, throw error if (global._documentLocks[lockKey] !== undefined) { throw new DocuDBError(`Document locked after ${maxRetries} attempts: ${id}`, MCO_ERROR.DOCUMENT.LOCK_ERROR); } // Set lock ; global._documentLocks[lockKey] = true; try { // Save updated document const chunkPaths = await this.storage.saveData(path.join(this.name, id), updatedDoc); // Delete old chunks if different if (JSON.stringify(this.documents[id].chunkPaths) !== JSON.stringify(chunkPaths)) { await this.storage.deleteChunks(this.documents[id].chunkPaths); } // Update cache this.documents[id] = { _id: id, data: updatedDoc, chunkPaths }; // Update metadata this.metadata.updated = new Date(); await this._saveMetadata(); // Update indices await this.indexManager.updateIndex(this.name, id, updatedDoc); } finally { // Release lock delete global._documentLocks[lockKey]; } return updatedDoc; } catch (error) { throw new DocuDBError(`Error updating document: ${error.message}`, MCO_ERROR.DOCUMENT.UPDATE_ERROR, { collectionName: this.name, id, originalError: error }); } } /** * Updates documents matching criteria * @param {QueryCriteria} criteria - Search criteria * @param {Object} update - Changes to apply * @returns {Promise<number>} - Number of documents updated */ async updateMany(criteria, update) { try { // Find matching documents const docs = await this.find(criteria); let count = 0; // Update each document for (const doc of docs) { const result = await this.updateById(doc._id, update); if (result != null) count++; } return count; } catch (error) { throw new DocuDBError(`Error updating documents: ${error.message}`, MCO_ERROR.DOCUMENT.UPDATE_ERROR, { collectionName: this.name, originalError: error }); } } /** * Deletes a document by its ID * @param {string} id - Document ID * @returns {Promise<boolean>} - true if successfully deleted */ async deleteById(id) { try { // Check if we have a schema with custom ID validation let skipDefaultIdValidation = false; if (this.schema?.definition?._id?.validate?.pattern !== undefined) { // If we have a schema with pattern validation for _id, we'll skip the default validation skipDefaultIdValidation = true; } // Validate ID format if not using custom schema validation if (!skipDefaultIdValidation) { if (typeof id !== 'string' || !isValidID(id)) { throw new DocuDBError('Invalid document ID format. Must be a valid MongoDB ID or UUID v4', MCO_ERROR.DOCUMENT.INVALID_ID, { id }); } } else if (typeof id !== 'string') { throw new DocuDBError('Invalid document ID format', MCO_ERROR.DOCUMENT.INVALID_ID, { id }); } // Check if exists if ((await this.findById(id)) == null) { return false; } // Delete chunks if (this.documents[id]?.chunkPaths !== undefined) { await this.storage.deleteChunks(this.documents[id].chunkPaths); } // Delete document directory const docDir = path.join(this.storage.dataDir, this.name, id); if (await fileExists(docDir)) { await promisify(fs.rm)(docDir, { recursive: true }); } // Remove from indices await this.indexManager.removeFromIndices(this.name, id); // Remove from cache delete this.documents[id]; // Update metadata this.metadata.count = Math.max(0, this.metadata.count - 1); this.metadata.updated = new Date(); // Remove document ID from the order array if (Array.isArray(this.metadata.documentOrder)) { this.metadata.documentOrder = this.metadata.documentOrder.filter(docId => docId !== id); } await this._saveMetadata(); return true; } catch (error) { throw new DocuDBError(`Error deleting document: ${error.message}`, MCO_ERROR.DOCUMENT.DELETE_ERROR, { collectionName: this.name, id, originalError: error }); } } /** * Deletes the first document found with the query criteria * @param {QueryCriteria} criteria - Search criteria * @returns {Promise<boolean>} - true if successfully deleted */ async deleteOne(criteria) { const result = await this.findOne(criteria); if (result === null) { return false; } return await this.deleteById(result._id); } /** * Deletes documents matching criteria * @param {QueryCriteria} criteria - Search criteria * @returns {Promise<number>} - Number of documents deleted */ async deleteMany(criteria) { try { // Find matching documents const docs = await this.find(criteria); let count = 0; // Delete each document for (const doc of docs) { const result = await this.deleteById(doc._id); if (result) count++; } return count; } catch (error) { throw new DocuDBError(`Error deleting documents: ${error.message}`, MCO_ERROR.DOCUMENT.DELETE_ERROR, { collectionName: this.name, originalError: error }); } } /** * Counts documents matching criteria * @param {QueryCriteria} criteria - Search criteria * @returns {Promise<number>} - Number of documents */ async count(criteria = {}) { try { if (Object.keys(criteria).length === 0) { // If no criteria, return metadata counter return this.metadata.count; } // If criteria exists, count matching documents const docs = await this.find(criteria); return docs.length; } catch (error) { throw new DocuDBError(`Error counting documents: ${error.message}`, MCO_ERROR.DOCUMENT.QUERY_ERROR, { collectionName: this.name, originalError: error }); } } /** * Creates an index for a field * @param {string} field - Field to index * @param {Object} options - Index options * @returns {Promise<boolean>} - true if successfully created */ async createIndex(field, options = {}) { try { // Create index await this.indexManager.createIndex(this.name, field, options); // Update metadata if (!this.metadata.indices.some(idx => idx.field === field)) { this.metadata.indices.push({ field, options }); this.metadata.updated = new Date(); await this._saveMetadata(); } // Index existing documents const allDocs = await this._loadAllDocuments(); for (const doc of allDocs) { await this.indexManager.updateIndex(this.name, doc._id, doc); } return true; } catch (error) { throw new DocuDBError(`Error creating index: ${error.message}`, MCO_ERROR.INDEX.CREATE_ERROR, { collectionName: this.name, field, originalError: error }); } } /** * List all the indices in the collection * @returns {Promise<Index[]>} - List of indices */ async listIndexes() { try { const indices = []; for (const key in this.indexManager.indices) { if (key.startsWith(`${this.name}:`)) { const field = key.split(':')[1]; const index = this.indexManager.indices[key]; indices.push({ ...index, field, unique: index.unique ?? false, sparse: index.sparse ?? false }); } } return indices; } catch (error) { throw new DocuDBError(`Error listing indexes: ${error.message}`, MCO_ERROR.INDEX.CREATE_ERROR, { collectionName: this.name, originalError: error }); } } /** * Drops an index * @param {string} field - Indexed field * @returns {Promise<boolean>} - true if successfully dropped */ async dropIndex(field) { try { await this.indexManager.dropIndex(this.name, field); this.metadata.indices = this.metadata.indices.filter(idx => idx.field !== field); this.metadata.updated = new Date(); await this._saveMetadata(); return true; } catch (error) { throw new DocuDBError(`Error deleting index: ${error.message}`, MCO_ERROR.INDEX.DROP_ERROR, { collectionName: this.name, field, originalError: error }); } } /** * Gets the index of a document by its ID in the collection * @param {string} id - Document ID * @returns {Promise<number>} - Index of the document or -1 if not found */ async getPosition(id) { try { if (typeof id !== 'string') { throw new DocuDBError('Invalid ID', MCO_ERROR.DOCUMENT.INVALID_DOCUMENT); } // Validate that ID has correct format (24 hexadecimal characters) const hexPattern = /^[0-9a-f]{24}$/; if (!hexPattern.test(id)) { throw new DocuDBError('Invalid ID: must be a 24-character hexadecimal string', MCO_ERROR.DOCUMENT.INVALID_ID); } // Check if document exists const doc = await this.findById(id); if (doc == null) { return -1; } // Load all documents to get their order const allDocs = await this._loadAllDocuments(); // Find the index of the document with the given ID const index = allDocs.findIndex(doc => doc._id === id); return index; } catch (error) { throw new DocuDBError(`Error finding document index: ${error.message}`, MCO_ERROR.DOCUMENT.QUERY_ERROR, { collectionName: this.name, id, originalError: error }); } } /** * Finds a document by its index in the collection * @param {number} position - Index of the document in the collection * @returns {Promise<Object|null>} - Document at the specified index or null if not found */ async findByPosition(position) { try { if (typeof position !== 'number' || position < 0) { throw new DocuDBError('Invalid Position: must be a non-negative number', MCO_ERROR.DOCUMENT.INVALID_DOCUMENT); } // Load all documents to get their order const allDocs = await this._loadAllDocuments(); // Check if index is within bounds if (position >= allDocs.length) { return null; } // Return the document at the specified index return allDocs[position]; } catch (error) { throw new DocuDBError(`Error finding document by index: ${error.message}`, MCO_ERROR.DOCUMENT.QUERY_ERROR, { collectionName: this.name, index: position, originalError: error }); } } /** * Updates the position of a document in the collection * @param {string} id - Document ID * @param {number} newIndex - New index position for the document * @returns {Promise<boolean>} - true if successfully updated, false if document not found */ async updatePosition(id, newIndex) { try { if (typeof id !== 'string') { throw new DocuDBError('Invalid ID', MCO_ERROR.DOCUMENT.INVALID_DOCUMENT); } if (typeof newIndex !== 'number' || newIndex < 0) { throw new DocuDBError('Invalid index: must be a non-negative number', MCO_ERROR.DOCUMENT.INVALID_DOCUMENT); } // Find the document const doc = await this.findById(id); if (doc == null) { return false; } // Load all documents to get their order const allDocs = await this._loadAllDocuments(); // Find current index const currentIndex = allDocs.findIndex(doc => doc._id === id); if (currentIndex === -1) { return false; } // If new index is out of bounds, adjust it to the last position const adjustedNewIndex = Math.min(newIndex, allDocs.length - 1); // If the index is the same, no need to update if (currentIndex === adjustedNewIndex) { return true; } // Remove the document from its current position const docToMove = allDocs.splice(currentIndex, 1)[0]; // Insert it at the new position allDocs.splice(adjustedNewIndex, 0, docToMove); // Save the order of the IDs in the metadata this.metadata.documentOrder = allDocs.map(doc => doc._id); await this._saveMetadata(); // Update the document cache to reflect the new order this.documents = {}; for (const doc of allDocs) { // Reload documents in the cache to maintain consistency await this.findById(doc._id); } return true; } catch (error) { throw new DocuDBError(`Error updating document index: ${error.message}`, MCO_ERROR.DOCUMENT.UPDATE_ERROR, { collectionName: this.name, id, newIndex, originalError: error }); } } /** * Removes the collection * @returns {Promise<void>} */ async drop() { try { const allDocs = await this._loadAllDocuments(); for (const doc of allDocs) { await this.deleteById(doc._id); } const collectionDir = path.join(this.storage.dataDir, this.name); if (await fileExists(collectionDir)) { await promisify(fs.rm)(collectionDir, { recursive: true }); } this.documents = {}; this.metadata.count = 0; } catch (error) { throw new DocuDBError(`Error deleting collection: ${error.message}`, MCO_ERROR.COLLECTION.DROP_ERROR, { collectionName: this.name, originalError: error }); } } /** * Loads the collection metadata * @private */ async _loadMetadata() { try { if (await fileExists(this.metadataPath)) { const data = await readFilePromise(this.metadataPath, 'utf8'); this.metadata = JSON.parse(data); } else { await this._saveMetadata(); } } catch (error) { throw new DocuDBError(`Error loading metadata: ${error.message}`, MCO_ERROR.COLLECTION.METADATA_ERROR, { collectionName: this.name, originalError: error }); } } /** * Stores the collection's metadata * @private */ async _saveMetadata() { try { await writeFilePromise(this.metadataPath, JSON.stringify(this.metadata, null, 2)); } catch (error) { throw new DocuDBError(`Error saving metadata: ${error.message}`, MCO_ERROR.COLLECTION.METADATA_ERROR, { collectionName: this.name, originalError: error }); } } /** * Generates a unique ID * @returns {string} - Generated ID * @private */ _generateId() { // Check if UUID format is specified in options if (this.options.idType === 'uuid') { // Use crypto.randomUUID() for UUID v4 generation return crypto.randomUUID(); } // Default to MongoDB-style ID (12 bytes hex) return crypto.randomBytes(12).toString('hex'); } /** * Applies an update to a document * @param {Object} doc - Original document * @param {Object} update - Changes to apply * @returns {Object} - Updated document * @private */ _applyUpdate(doc, update) { const result = deepCopy(doc); if (update.$set == null && update.$unset == null && update.$inc == null) { const id = result._id; Object.assign(result, update); result._id = id; return result; } if (update.$set != null) { for (const [key, value] of Object.entries(update.$set)) { this._setNestedValue(result, key, value); } } if (update.$unset != null) { for (const key of Object.keys(update.$unset)) { this._unsetNestedValue(result, key); } } if (update.$inc != null) { for (const [key, value] of Object.entries(update.$inc)) { const currentValue = this._getNestedValue(result, key) ?? 0; if (typeof currentValue !== 'number') { throw new DocuDBError(`Cannot increment a non-numeric value: ${key}`, MCO_ERROR.DOCUMENT.INVALID_TYPE, { field: key, value: currentValue }); } this._setNestedValue(result, key, currentValue + value); } } return result; } /** * Sets a nested value in an object * @param {Object} obj - Object to modify * @param {string} path - Path to the value using dot notation * @param {*} value - Value to set * @private */ _setNestedValue(obj, path, value) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (current[part] === undefined) { current[part] = {}; } else if (typeof current[part] !== 'object') { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = value; } /** * Removes a nested value from an object * @param {Object} obj - Object to modify * @param {string} path - Path to the value using dot notation * @private */ _unsetNestedValue(obj, path) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (current[part] === undefined || typeof current[part] !== 'object') { return; } current = current[part]; } delete current[parts[parts.length - 1]]; } /** * Gets a nested value from an object * @param {Object} obj - Object to get the value from * @param {string} path - Path to the value using dot notation * @returns {*} - Found or undefined value * @private */ _getNestedValue(obj, path) { const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === null || current === undefined || typeof current !== 'object') { return undefined; } current = current[part]; } return current; } /** * Loads all documents in the collection * @returns {Promise<Document[]>} - All documents * @private */ async _loadAllDocuments() { try { const collectionDir = path.join(this.storage.dataDir, this.name); const items = await promisify(fs.readdir)(collectionDir, { withFileTypes: true }); const docs = []; const documentMap = {}; // First load all documents into a map for (const item of items) { if (item.isDirectory() && !item.name.startsWith('_')) { const docId = item.name; const doc = await this.findById(docId); if (doc != null) { documentMap[docId] = doc; } } } // If there's a defined order in metadata, use it to order the documents if (Array.isArray(this.metadata.documentOrder)) { // Add documents in the specified order for (const docId of this.metadata.documentOrder) { if (documentMap[docId] !== undefined) { docs.push(documentMap[docId]); delete documentMap[docId]; // Remove to avoid duplication } } // Add any document that's not in the order (new documents) for (const docId in documentMap) { docs.push(documentMap[docId]); } } else { // If there's no defined order, use the default order for (const docId in documentMap) { docs.push(documentMap[docId]); } } return docs; } catch (error) { throw new DocuDBError(`Error uploading documents: ${error.message}`, MCO_ERROR.DOCUMENT.NOT_FOUND, { collectionName: this.name, originalError: error }); } } /** * Attempts to optimize a query using indexes * @param {Query} query - Query to optimize * @returns {Promise<Document[]|null>} - Results or null if the optimization failed * @private */ async _findWithOptimization(query) { for (const field in query.criteria) { if (this.indexManager.hasIndex(this.name, field)) { const value = query.criteria[field]; if (typeof value !== 'object' || value === null) { const docIds = this.indexManager.findByIndex(this.name, field, value); if (docIds != null && docIds.length > 0) { const docs = []; for (const id of docIds) { const doc = await this.findById(id); if (doc != null && query.matches(doc)) { docs.push(doc); } } return query.execute(docs); } } } } return null; } } export default Database; //# sourceMappingURL=database.js.map