@sinkingsheep/jsondb
Version:
A lightweight JSON-based database for Node.js
382 lines • 17 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 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