nodedb-json
Version:
A lightweight JSON-based database for Node.js with TypeScript support, indexing, and complex query capabilities
700 lines • 27.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodedbJson = void 0;
const fs = __importStar(require("fs"));
const lodash_1 = __importDefault(require("lodash"));
const collection_1 = require("./collection/collection");
const index_codec_1 = require("./indexer/index-codec");
const index_manager_1 = require("./indexer/index-manager");
const matcher_1 = require("./query/matcher");
const query_engine_1 = require("./query/query-engine");
const sorter_1 = require("./query/sorter");
const file_lock_1 = require("./storage/file-lock");
const json_file_storage_1 = require("./storage/json-file-storage");
/**
* A class to manage JSON-based database operations.
*/
class NodedbJson {
/**
* Creates an instance of NodedbJson.
* @param {string} filePath - The path to the JSON file.
* @param {DbOptions} [options] - Database options.
*/
constructor(filePath, options = {}) {
this._pendingChanges = 0;
this._indexes = {};
this._indexDefinitions = {};
this.closed = false;
this.filePath = filePath;
this.options = {
autoSave: true,
createIfNotExists: true,
defaultValue: {},
enableIndexing: true,
autoIndex: true,
indexValueMode: 'strict',
persistIndexes: true,
atomicWrites: true,
backupOnWrite: true,
fileLock: true,
...options
};
this.indexMetaPath = this.options.indexMetaPath || `${this.filePath}.meta.json`;
this.backupPath = this.options.backupPath || `${this.filePath}.bak`;
this.tempPath = this.options.tempPath || `${this.filePath}.tmp`;
this.lockPath = this.options.lockPath || `${this.filePath}.lock`;
this.fileLock = new file_lock_1.FileLock({
enabled: this.options.fileLock !== false,
lockPath: this.lockPath,
staleLockMs: this.options.staleLockMs
});
this.fileLock.acquire();
try {
this.storage = this._createStorage(this.filePath, this.tempPath, this.backupPath, this.options.defaultValue || {}, this.options.createIfNotExists !== false);
this.metaStorage = this._createStorage(this.indexMetaPath, `${this.indexMetaPath}.tmp`, `${this.indexMetaPath}.bak`, { indexes: {} }, false);
this.data = this.readJSONFile();
this._indexDefinitions = this._loadIndexDefinitions();
// 自动创建索引
if (this.options.enableIndexing && this.options.autoIndex) {
this._rebuildAllIndexes();
}
}
catch (error) {
this.fileLock.release();
throw error;
}
}
readJSONFile() {
this._assertOpen();
return this.storage.read();
}
writeJSONFile() {
this._assertOpen();
this.storage.write(JSON.stringify(this.data, null, 2));
this._pendingChanges = 0;
}
_createStorage(filePath, tempPath, backupPath, defaultValue, createIfNotExists) {
return new json_file_storage_1.JsonFileStorage(filePath, {
atomicWrites: this.options.atomicWrites !== false,
backupOnWrite: this.options.backupOnWrite !== false,
backupPath,
createIfNotExists,
defaultValue,
tempPath
});
}
_loadIndexDefinitions() {
const definitions = {};
if (this.options.persistIndexes && fs.existsSync(this.indexMetaPath)) {
const parsedMeta = this.metaStorage.read();
const persistedIndexes = parsedMeta.indexes || parsedMeta;
(0, index_codec_1.mergeIndexDefinitions)(definitions, persistedIndexes);
}
if (this.options.indexes) {
(0, index_codec_1.mergeIndexDefinitions)(definitions, this.options.indexes);
}
return definitions;
}
_saveIndexDefinitions() {
this._assertOpen();
if (!this.options.persistIndexes) {
return;
}
this.metaStorage.write(JSON.stringify({ indexes: this._indexDefinitions }, null, 2));
}
_getReference(key) {
this._assertOpen();
return lodash_1.default.get(this.data, key);
}
_assertOpen() {
if (this.closed) {
throw new Error('Database instance is closed.');
}
}
/**
* Manually save changes to file.
* @returns {NodedbJson} - The instance of the database for chaining.
*/
save() {
this._assertOpen();
if (this._pendingChanges > 0) {
this.writeJSONFile();
}
return this;
}
/**
* Writes pending changes to disk and returns this instance.
*/
flush() {
this._assertOpen();
this.writeJSONFile();
return this;
}
/**
* Reloads data and persisted index definitions from disk.
*/
reload() {
this._assertOpen();
this.data = this.readJSONFile();
this._indexDefinitions = this._loadIndexDefinitions();
this._indexes = {};
this._pendingChanges = 0;
if (this.options.enableIndexing && this.options.autoIndex) {
this._rebuildAllIndexes();
}
return this;
}
/**
* Flushes pending changes and releases the process-level file lock.
*/
close(options = {}) {
if (this.closed) {
return;
}
if (!options.force) {
this.save();
}
this.closed = true;
this.fileLock.release();
}
set(key, value) {
this._assertOpen();
// 检查是否需要更新索引
const oldValue = lodash_1.default.get(this.data, key);
const needsIndexUpdate = this.options.enableIndexing &&
(Array.isArray(oldValue) || Array.isArray(value)) &&
this._hasIndexDefinition(key);
if (this.options.enableIndexing && Array.isArray(value)) {
this._assertUniqueIndexesForKey(key, value);
}
lodash_1.default.set(this.data, key, value);
this._pendingChanges++;
// 如果修改了带索引的数组,重建索引
if (needsIndexUpdate) {
this._rebuildIndexesForKey(key);
}
if (this.options.autoSave) {
this.writeJSONFile();
}
return this;
}
get(key) {
return lodash_1.default.cloneDeep(this._getReference(key));
}
getUnsafeReference(key) {
return this._getReference(key);
}
mutate(key, mutator) {
const value = this._getReference(key);
if (value === undefined) {
throw new Error(`Key "${key}" does not exist.`);
}
const nextValue = lodash_1.default.cloneDeep(value);
mutator(nextValue);
if (this.options.enableIndexing && Array.isArray(nextValue)) {
this._assertUniqueIndexesForKey(key, nextValue);
}
lodash_1.default.set(this.data, key, nextValue);
this._pendingChanges++;
if (this.options.enableIndexing && this._hasIndexDefinition(key)) {
this._rebuildIndexesForKey(key);
}
if (this.options.autoSave) {
this.writeJSONFile();
}
return this;
}
/**
* Checks if a key exists in the JSON data.
* @param {string} key - The key to check.
* @returns {boolean} - True if the key exists, otherwise false.
*/
has(key) {
this._assertOpen();
return lodash_1.default.has(this.data, key);
}
update(key, predicateOrUpdater, updater) {
const data = this._getReference(key);
if (Array.isArray(data)) {
if (!updater) {
throw new Error(`Updater object must be provided for array updates.`);
}
// 如果使用索引进行更新
if (typeof predicateOrUpdater === 'object' &&
this.options.enableIndexing &&
this._hasIndexDefinition(key)) {
// 尝试使用索引查找
const foundIndex = this._findIndexedItemPosition(key, predicateOrUpdater, data);
if (foundIndex !== -1) {
const nextData = lodash_1.default.cloneDeep(data);
lodash_1.default.merge(nextData[foundIndex], updater);
this._assertUniqueIndexesForKey(key, nextData);
lodash_1.default.merge(data[foundIndex], updater);
this._pendingChanges++;
// 更新索引
this._rebuildIndexesForKey(key);
}
else {
throw new Error(`No item found matching the predicate.`);
}
}
else {
// 常规查找
const predicate = typeof predicateOrUpdater === 'function'
? predicateOrUpdater
: (item) => (0, matcher_1.matchesConditions)(item, predicateOrUpdater);
const item = lodash_1.default.find(data, predicate);
if (item) {
const nextData = lodash_1.default.cloneDeep(data);
const foundIndex = data.indexOf(item);
lodash_1.default.merge(nextData[foundIndex], updater);
if (this.options.enableIndexing) {
this._assertUniqueIndexesForKey(key, nextData);
}
lodash_1.default.merge(item, updater);
this._pendingChanges++;
// 如果有索引,需要更新
if (this.options.enableIndexing && this._hasIndexDefinition(key)) {
this._rebuildIndexesForKey(key);
}
}
else if (!item) {
throw new Error(`No item found matching the predicate.`);
}
}
}
else if (lodash_1.default.isObject(data)) {
if (typeof predicateOrUpdater === 'function') {
lodash_1.default.update(this.data, key, predicateOrUpdater);
}
else {
lodash_1.default.merge(data, predicateOrUpdater);
}
this._pendingChanges++;
}
else {
throw new Error(`Key "${key}" does not reference a collection or array.`);
}
if (this.options.autoSave) {
this.writeJSONFile();
}
return this;
}
delete(key, predicateOrKeys, field = 'id') {
const data = this._getReference(key);
const hasIndex = this.options.enableIndexing && this._hasIndexDefinition(key);
if (Array.isArray(data)) {
if (typeof predicateOrKeys === 'function') {
lodash_1.default.remove(data, predicateOrKeys);
}
else if (Array.isArray(predicateOrKeys)) {
// 如果有索引,尝试使用索引删除
if (hasIndex && this._hasBuiltIndexOnField(key, field)) {
const indexes = lodash_1.default.uniq(predicateOrKeys.flatMap(fieldValue => this._getItemIndexesByField(key, field, fieldValue))).sort((a, b) => b - a);
for (const index of indexes) {
data.splice(index, 1);
}
}
else {
if (this._getIndexValueMode() === 'coerce') {
const valuesToDelete = new Set(predicateOrKeys.map(value => String(value)));
lodash_1.default.remove(data, (item) => valuesToDelete.has(String(item[field])));
}
else {
lodash_1.default.remove(data, (item) => predicateOrKeys.includes(item[field]));
}
}
}
else {
throw new Error(`Predicate or keys array must be provided for array deletion.`);
}
}
else if (this.has(key)) {
if (Array.isArray(predicateOrKeys)) {
predicateOrKeys.forEach(itemKey => {
lodash_1.default.unset(data, itemKey);
});
}
else {
lodash_1.default.unset(this.data, key);
}
}
else {
throw new Error(`Key "${key}" does not exist.`);
}
// 如果有索引,更新索引
if (hasIndex) {
this._rebuildIndexesForKey(key);
}
this._pendingChanges++;
if (this.options.autoSave) {
this.writeJSONFile();
}
return this;
}
find(key, predicate) {
const data = this._getReference(key);
if (Array.isArray(data)) {
return lodash_1.default.cloneDeep(lodash_1.default.find(data, predicate));
}
else if (lodash_1.default.isObject(data)) {
return lodash_1.default.cloneDeep(lodash_1.default.find(Object.values(data), predicate));
}
else {
throw new Error(`Key "${key}" does not reference a collection or array.`);
}
}
findByField(key, field, value) {
const data = this._getReference(key);
if (!Array.isArray(data)) {
throw new Error(`Key "${key}" does not reference an array.`);
}
// 使用索引进行查找
if (this.options.enableIndexing && this._hasBuiltIndexOnField(key, field)) {
const index = this._getItemIndexByField(key, field, value);
return index !== -1 ? lodash_1.default.cloneDeep(data[index]) : undefined;
}
// 常规查找
return lodash_1.default.cloneDeep(lodash_1.default.find(data, item => item[field] === value));
}
filter(key, predicate) {
const data = this._getReference(key);
if (Array.isArray(data)) {
return lodash_1.default.cloneDeep(lodash_1.default.filter(data, predicate));
}
else if (lodash_1.default.isObject(data)) {
return lodash_1.default.cloneDeep(lodash_1.default.filter(Object.values(data), predicate));
}
else {
throw new Error(`Key "${key}" does not reference a collection or array.`);
}
}
filterByField(key, field, values) {
const data = this._getReference(key);
if (!Array.isArray(data)) {
throw new Error(`Key "${key}" does not reference an array.`);
}
// 使用索引进行过滤
if (this.options.enableIndexing && this._hasBuiltIndexOnField(key, field)) {
const result = [];
for (const value of values) {
const indexes = this._getItemIndexesByField(key, field, value);
for (const index of indexes) {
result.push(data[index]);
}
}
return lodash_1.default.cloneDeep(result);
}
// 常规过滤
return lodash_1.default.cloneDeep(lodash_1.default.filter(data, item => values.includes(item[field])));
}
push(key, value) {
if (!this.has(key)) {
if (Array.isArray(value)) {
this.set(key, value);
}
else {
this.set(key, [value]);
}
}
else {
const array = this._getReference(key);
if (Array.isArray(array)) {
if (this.options.enableIndexing) {
const nextArray = Array.isArray(value)
? array.concat(value)
: array.concat([value]);
this._assertUniqueIndexesForKey(key, nextArray);
}
if (Array.isArray(value)) {
array.push(...value);
}
else {
array.push(value);
}
this._pendingChanges++;
// 如果有索引,更新索引
if (this.options.enableIndexing && this._hasIndexDefinition(key)) {
this._rebuildIndexesForKey(key);
}
if (this.options.autoSave) {
this.writeJSONFile();
}
}
else {
throw new Error(`Key "${key}" is not an array.`);
}
}
return this;
}
/**
* Executes multiple operations in batch.
* @param {Array<{method: string, args: any[]}>} operations - Array of operations to execute.
* @returns {NodedbJson} - The instance of the database for chaining.
*/
batch(operations) {
this._assertOpen();
const originalAutoSave = this.options.autoSave;
const originalData = lodash_1.default.cloneDeep(this.data);
const originalIndexes = lodash_1.default.cloneDeep(this._indexes);
const originalPendingChanges = this._pendingChanges;
this.options.autoSave = false;
try {
operations.forEach(op => {
const { method, args } = op;
const batchMethod = this[method];
if (!NodedbJson.BATCH_ALLOWED_METHODS.has(method) || typeof batchMethod !== 'function') {
throw new Error(`Invalid batch method: ${method}`);
}
batchMethod.apply(this, args);
});
this.writeJSONFile();
}
catch (error) {
this.data = originalData;
this._indexes = originalIndexes;
this._pendingChanges = originalPendingChanges;
throw error;
}
finally {
this.options.autoSave = originalAutoSave;
}
return this;
}
createIndex(key, indexDefinition) {
if (!this.options.enableIndexing) {
throw new Error('Indexing is not enabled. Set enableIndexing option to true.');
}
(0, index_codec_1.validateIndexDefinition)(indexDefinition);
const data = this._getReference(key);
if (!Array.isArray(data)) {
throw new Error(`Cannot create index on non-array data at "${key}"`);
}
if (indexDefinition.type === 'unique') {
this._assertUniqueIndexForDefinition(key, data, indexDefinition);
}
const originalDefinitions = lodash_1.default.cloneDeep(this._indexDefinitions);
const originalIndexes = lodash_1.default.cloneDeep(this._indexes);
try {
// 保存索引定义
if (!this._indexDefinitions[key]) {
this._indexDefinitions[key] = {};
}
this._indexDefinitions[key][indexDefinition.field] = indexDefinition;
// 创建索引
this._buildIndex(key, indexDefinition);
this._saveIndexDefinitions();
}
catch (error) {
this._indexDefinitions = originalDefinitions;
this._indexes = originalIndexes;
throw error;
}
return this;
}
dropIndex(key, field) {
this._assertOpen();
if (!this.options.enableIndexing) {
return this;
}
if (this._indexDefinitions[key] && this._indexDefinitions[key][field]) {
const originalDefinitions = lodash_1.default.cloneDeep(this._indexDefinitions);
const originalIndexes = lodash_1.default.cloneDeep(this._indexes);
try {
delete this._indexDefinitions[key][field];
// 如果没有索引了,删除整个键
if (Object.keys(this._indexDefinitions[key]).length === 0) {
delete this._indexDefinitions[key];
}
// 删除索引数据
const indexKey = `${key}:${field}`;
if (this._indexes[indexKey]) {
delete this._indexes[indexKey];
}
this._saveIndexDefinitions();
}
catch (error) {
this._indexDefinitions = originalDefinitions;
this._indexes = originalIndexes;
throw error;
}
}
return this;
}
/**
* 获取所有索引信息
* @returns {Record<string, Record<string, IndexDefinition>>} - 索引定义
*/
getIndexes() {
this._assertOpen();
return lodash_1.default.cloneDeep(this._indexDefinitions);
}
_hasIndexDefinition(key) {
return (0, index_manager_1.hasIndexDefinition)(this._indexDefinitions, key);
}
_hasIndexOnField(key, field) {
return (0, index_manager_1.hasIndexOnField)(this._indexDefinitions, key, field);
}
_hasBuiltIndexOnField(key, field) {
return Object.prototype.hasOwnProperty.call(this._indexes, `${key}:${field}`);
}
_rebuildAllIndexes() {
// 清空所有索引
this._indexes = {};
// 重建每个定义的索引
for (const key in this._indexDefinitions) {
for (const field in this._indexDefinitions[key]) {
this._buildIndex(key, this._indexDefinitions[key][field]);
}
}
}
_rebuildIndexesForKey(key) {
if (!this._indexDefinitions[key]) {
return;
}
// 重建该路径的所有索引
for (const field in this._indexDefinitions[key]) {
this._buildIndex(key, this._indexDefinitions[key][field]);
}
}
_assertUniqueIndexesForKey(key, data) {
(0, index_manager_1.assertUniqueIndexesForKey)(this._indexDefinitions, key, data, this._getIndexValueMode());
}
_assertUniqueIndexForDefinition(key, data, indexDefinition) {
(0, index_manager_1.assertUniqueIndexForDefinition)(key, data, indexDefinition, this._getIndexValueMode());
}
_buildIndex(key, indexDef) {
const data = this._getReference(key);
if (!Array.isArray(data)) {
return;
}
(0, index_manager_1.buildIndex)(this._indexes, key, data, indexDef, this._getIndexValueMode());
}
_findIndexedItemPosition(key, obj, data) {
// 检查是否有匹配的索引
for (const field in obj) {
if (this._hasBuiltIndexOnField(key, field)) {
const indexes = this._getItemIndexesByField(key, field, obj[field]);
const index = indexes.find(itemIndex => (0, matcher_1.matchesConditions)(data[itemIndex], obj));
if (index !== undefined) {
return index;
}
}
}
return -1;
}
_getItemIndexByField(key, field, value) {
return (0, index_manager_1.getItemIndexByField)(this._indexes, key, field, value, this._getIndexValueMode());
}
_getItemIndexesByField(key, field, value) {
return (0, index_manager_1.getItemIndexesByField)(this._indexes, key, field, value, this._getIndexValueMode());
}
_getIndexValueMode() {
return this.options.indexValueMode === 'coerce' ? 'coerce' : 'strict';
}
query(key, options = {}) {
const data = this._getReference(key);
if (!Array.isArray(data)) {
throw new Error(`Key "${key}" does not reference an array.`);
}
const engine = new query_engine_1.QueryEngine({
getItemIndexesByField: (field, value) => this._getItemIndexesByField(key, field, value),
hasIndexOnField: field => this.options.enableIndexing === true && this._hasBuiltIndexOnField(key, field)
});
return engine.execute(data, options);
}
_applySort(data, sort) {
return (0, sorter_1.applySort)(data, sort);
}
orderBy(key, sort, limit) {
const data = this._getReference(key);
if (!Array.isArray(data)) {
throw new Error(`Key "${key}" does not reference an array.`);
}
const sortOptions = Array.isArray(sort) ? sort : [sort];
let sortedData = lodash_1.default.orderBy(data, sortOptions.map(option => option.field), sortOptions.map(option => option.direction));
if (limit && limit > 0) {
sortedData = sortedData.slice(0, limit);
}
return lodash_1.default.cloneDeep(sortedData);
}
paginate(key, page, pageSize, where) {
const result = this.query(key, {
where,
pagination: { page, pageSize }
});
return {
data: result.data,
pagination: result.pagination
};
}
aggregate(key, aggregations, where) {
const result = this.query(key, {
where,
aggregation: aggregations
});
return result.aggregations || [];
}
count(key, where) {
var _a;
const result = this.aggregate(key, [{ type: 'count' }], where);
return ((_a = result[0]) === null || _a === void 0 ? void 0 : _a.value) || 0;
}
distinct(key, field) {
const data = this._getReference(key);
if (!Array.isArray(data)) {
throw new Error(`Key "${key}" does not reference an array.`);
}
return lodash_1.default.cloneDeep(lodash_1.default.uniqBy(data, field).map(item => lodash_1.default.get(item, field)));
}
collection(key) {
this._assertOpen();
return new collection_1.Collection(this, key);
}
}
exports.NodedbJson = NodedbJson;
NodedbJson.BATCH_ALLOWED_METHODS = new Set([
'set',
'update',
'delete',
'push',
'mutate'
]);
exports.default = NodedbJson;
//# sourceMappingURL=database.js.map