UNPKG

@sinkingsheep/jsondb

Version:

A lightweight JSON-based database for Node.js

382 lines 17 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 fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const events_1 = require("events"); const fileOperations_1 = require("./fileOperations"); const autoSaveManager_1 = require("./autoSaveManager"); const utils_1 = require("./utils"); const operators_1 = require("./operators"); const queryChain_1 = require("./queryChain"); class JsonDB extends events_1.EventEmitter { constructor(options) { var _a, _b, _c; super(); this.directory = options.directory; this.collections = new Map(); this.dirty = new Set(); this.prettyPrint = (_a = options.prettyPrint) !== null && _a !== void 0 ? _a : false; this.schemas = new Map(); this.indexes = new Map(); this.indexConfigs = new Map(); this.transactions = new Map(); this.autoSaveManager = new autoSaveManager_1.AutoSaveManager((_b = options.autoSave) !== null && _b !== void 0 ? _b : true, (_c = options.saveInterval) !== null && _c !== void 0 ? _c : 1000, () => this.saveAll()); } createIndex(collection_1, field_1) { return __awaiter(this, arguments, void 0, function* (collection, field, config = {}) { if (!this.indexes.has(collection)) { this.indexes.set(collection, new Map()); } const collectionIndexes = this.indexes.get(collection); collectionIndexes.set(field, new Map()); // Store index configuration separately if (!this.indexConfigs.has(collection)) { this.indexConfigs.set(collection, new Map()); } const configMap = this.indexConfigs.get(collection); configMap.set(field, config); // Build index for existing documents const docs = yield this.find(collection, {}); const fieldIndex = collectionIndexes.get(field); for (const doc of docs) { const value = doc[field]; if (value === undefined && config.sparse) { continue; } if (!fieldIndex.has(value)) { fieldIndex.set(value, new Set()); } fieldIndex.get(value).add(doc.id); if (config.unique && fieldIndex.get(value).size > 1) { collectionIndexes.delete(field); throw new Error(`Unique constraint violation for field ${field}`); } } }); } init() { return __awaiter(this, void 0, void 0, function* () { try { yield fs_1.promises.mkdir(this.directory, { recursive: true }); } catch (error) { throw new Error(`Failed to create database directory: ${error}`); } this.autoSaveManager.start(); }); } loadCollection(collectionName) { return __awaiter(this, void 0, void 0, function* () { if (this.collections.has(collectionName)) return; const collection = yield (0, fileOperations_1.loadCollection)(this.directory, collectionName); this.collections.set(collectionName, collection); }); } saveCollection(collectionName) { return __awaiter(this, void 0, void 0, function* () { const collection = this.collections.get(collectionName); if (!collection) return; yield (0, fileOperations_1.saveCollection)(this.directory, collectionName, collection, this.prettyPrint); this.dirty.delete(collectionName); this.emit("save", collectionName); }); } saveAll() { return __awaiter(this, void 0, void 0, function* () { const promises = Array.from(this.dirty).map((name) => this.saveCollection(name)); yield Promise.all(promises); }); } insert(collection, document) { return __awaiter(this, void 0, void 0, function* () { yield this.loadCollection(collection); const col = this.collections.get(collection); const id = (0, utils_1.generateId)(document.id); const docWithId = Object.assign(Object.assign({}, document), { id }); // Check indexes if (this.indexes.has(collection)) { const collectionIndexes = this.indexes.get(collection); const configMap = this.indexConfigs.get(collection); for (const [field, fieldIndex] of collectionIndexes.entries()) { if (fieldIndex instanceof Map) { const value = docWithId[field]; const config = configMap.get(field); // Skip if value is undefined and index is sparse if (value === undefined && (config === null || config === void 0 ? void 0 : config.sparse)) continue; if (!fieldIndex.has(value)) { fieldIndex.set(value, new Set()); } const documents = fieldIndex.get(value); if (documents.size > 0 && (config === null || config === void 0 ? void 0 : config.unique)) { throw new Error(`Unique constraint violation for field ${field}`); } documents.add(docWithId.id); } } } col.set(id, docWithId); this.dirty.add(collection); return docWithId; }); } insertMany(collectionName, documents) { return __awaiter(this, void 0, void 0, function* () { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); const results = []; for (const doc of documents) { const id = (0, utils_1.generateId)(doc.id); const docWithId = Object.assign(Object.assign({}, doc), { id }); collection.set(id, docWithId); results.push(docWithId); } // Mark collection as dirty and save immediately this.dirty.add(collectionName); yield this.saveCollection(collectionName); return results; }); } findWithJoin(collectionName_1) { return __awaiter(this, arguments, void 0, function* (collectionName, query = {}, options = {}) { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); let results = Array.from(collection.values()).filter((doc) => Object.entries(query).every(([key, value]) => doc[key] === value)); // Apply basic paging if (options.skip) { results = results.slice(options.skip); } if (options.limit) { results = results.slice(0, options.limit); } // Handle optional relational joins if (options.joins && options.joins.length) { for (const join of options.joins) { yield this.loadCollection(join.collection); const foreignCollection = this.collections.get(join.collection); const foreignDocs = Array.from(foreignCollection.values()); // For each result, find related docs results = results.map((item) => { const related = foreignDocs.filter((fd) => fd[join.foreignField] === item[join.localField]); return Object.assign(Object.assign({}, item), { [join.as || join.collection]: related }); }); } } return results; }); } find(collectionName_1) { return __awaiter(this, arguments, void 0, function* (collectionName, query = {}, options = {}) { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); let results = Array.from(collection.values()); // console.log('Loaded documents:', results); if (Object.keys(query).length > 0) { results = results.filter(doc => (0, operators_1.matchesQuery)(doc, query)); } // Apply sort and pagination if (options.sort) { results = this.applySorting(results, options.sort); } if (options.skip) { results = results.slice(options.skip); } if (options.limit) { results = results.slice(0, options.limit); } return results; }); } findOne(collectionName_1) { return __awaiter(this, arguments, void 0, function* (collectionName, query = {}) { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); const results = yield this.find(collectionName, query, { limit: 1 }); return results[0] || null; }); } update(collectionName, query, update) { return __awaiter(this, void 0, void 0, function* () { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); let updateCount = 0; // Handle special case for accounts collection if (collectionName === 'accounts' && 'balance' in update && update.balance < 0) { throw new Error('Balance cannot be negative'); } for (const [id, doc] of collection.entries()) { if (Object.entries(query).every(([key, value]) => doc[key] === value)) { const updatedDoc = Object.assign(Object.assign({}, doc), update); collection.set(id, updatedDoc); updateCount++; this.emit("update", collectionName, updatedDoc); this.dirty.add(collectionName); } } return updateCount; }); } updateOne(collectionName, query, update) { return __awaiter(this, void 0, void 0, function* () { const updateCount = yield this.update(collectionName, query, update); return updateCount > 0; }); } delete(collectionName, query) { return __awaiter(this, void 0, void 0, function* () { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); let deleteCount = 0; for (const [id, doc] of collection.entries()) { if (Object.entries(query).every(([key, value]) => doc[key] === value)) { collection.delete(id); deleteCount++; this.emit("delete", collectionName, id); } } if (deleteCount > 0) { this.dirty.add(collectionName); } return deleteCount; }); } dropCollection(collectionName) { return __awaiter(this, void 0, void 0, function* () { const filePath = path_1.default.join(this.directory, `${collectionName}.json`); try { yield fs_1.promises.unlink(filePath); this.collections.delete(collectionName); this.dirty.delete(collectionName); this.emit("drop", collectionName); } catch (error) { if (error.code !== "ENOENT") { throw new Error(`Failed to drop collection ${collectionName}: ${error}`); } } }); } beginTransaction() { return __awaiter(this, void 0, void 0, function* () { const transactionId = (0, utils_1.generateId)(); const snapshots = new Map(); // Take deep copy snapshots of all collections for (const [name, collection] of this.collections.entries()) { const snapshot = new Map(); for (const [id, doc] of collection.entries()) { snapshot.set(id, JSON.parse(JSON.stringify(doc))); } snapshots.set(name, snapshot); } this.transactions.set(transactionId, { id: transactionId, operations: [], status: "pending", snapshots }); return transactionId; }); } takeCollectionSnapshot(collectionName) { return __awaiter(this, void 0, void 0, function* () { yield this.loadCollection(collectionName); const collection = this.collections.get(collectionName); if (!collection) return new Map(); return new Map(collection); }); } commitTransaction(transactionId) { return __awaiter(this, void 0, void 0, function* () { const transaction = this.transactions.get(transactionId); if (!transaction) { throw new Error("Transaction not found"); } if (transaction.status !== "pending") { throw new Error("Transaction is not pending"); } try { for (const operation of transaction.operations) { switch (operation.type) { case "insert": yield this.insert(operation.collection, operation.document); break; case "update": yield this.update(operation.collection, operation.query, operation.document); break; case "delete": yield this.delete(operation.collection, operation.query); break; } } transaction.status = "committed"; yield this.saveAll(); } catch (error) { // Ensure we rollback on any error yield this.rollbackTransaction(transactionId); throw error; } }); } rollbackTransaction(transactionId) { return __awaiter(this, void 0, void 0, function* () { const transaction = this.transactions.get(transactionId); if (!transaction) { throw new Error("Transaction not found"); } // Deep copy restore from snapshots for (const [collectionName, snapshot] of transaction.snapshots.entries()) { const restoredCollection = new Map(); for (const [id, doc] of snapshot.entries()) { restoredCollection.set(id, JSON.parse(JSON.stringify(doc))); } this.collections.set(collectionName, restoredCollection); this.dirty.add(collectionName); } transaction.status = "rolled_back"; yield this.saveAll(); this.transactions.delete(transactionId); }); } chain(collectionName) { return new queryChain_1.QueryChain(this, collectionName); } applySorting(results, sort) { const sortEntries = Object.entries(sort); return [...results].sort((a, b) => { for (const [key, order] of sortEntries) { if (a[key] < b[key]) return -1 * order; if (a[key] > b[key]) return 1 * order; } return 0; }); } close() { return __awaiter(this, void 0, void 0, function* () { this.autoSaveManager.stop(); yield this.saveAll(); this.collections.clear(); this.dirty.clear(); this.emit("close"); }); } } exports.default = JsonDB; //# sourceMappingURL=JsonDB.js.map