json-database-st 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/JSONDatabase.js +256 -160
  2. package/package.json +3 -2
package/JSONDatabase.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // File: JSONDatabase.js
2
2
  // Final, Complete, and Secure Version (Patched)
3
3
 
4
- const fs = require('fs').promises;
5
- const path = require('path');
6
- const crypto = require('crypto');
7
- const _ = require('lodash');
8
- const EventEmitter = require('events');
4
+ const fs = require("fs").promises;
5
+ const path = require("path");
6
+ const crypto = require("crypto");
7
+ const _ = require("lodash");
8
+ const EventEmitter = require("events");
9
+ const lockfile = require("proper-lockfile");
9
10
 
10
11
  // --- Custom Error Classes for Better Error Handling ---
11
12
 
@@ -32,7 +33,6 @@ class IndexViolationError extends DBError {}
32
33
  /** Error for security-related issues like path traversal or bad keys. */
33
34
  class SecurityError extends DBError {}
34
35
 
35
-
36
36
  // --- Type Definitions for Clarity ---
37
37
 
38
38
  /**
@@ -74,13 +74,11 @@ class SecurityError extends DBError {}
74
74
  * @property {boolean} [unique=false] - If true, enforces that the indexed field must be unique across the collection.
75
75
  */
76
76
 
77
-
78
77
  // --- Cryptography Constants ---
79
- const ALGORITHM = 'aes-256-gcm';
78
+ const ALGORITHM = "aes-256-gcm";
80
79
  const IV_LENGTH = 16;
81
80
  const AUTH_TAG_LENGTH = 16;
82
81
 
83
-
84
82
  /**
85
83
  * A robust, secure, promise-based JSON file database with atomic operations, indexing, schema validation, and events.
86
84
  * Includes encryption-at-rest and path traversal protection.
@@ -109,13 +107,23 @@ class JSONDatabase extends EventEmitter {
109
107
  const resolvedPath = path.resolve(filename);
110
108
  const workingDir = process.cwd();
111
109
  if (!resolvedPath.startsWith(workingDir)) {
112
- throw new SecurityError(`Path traversal detected. Database path must be within the project directory: ${workingDir}`);
110
+ throw new SecurityError(
111
+ `Path traversal detected. Database path must be within the project directory: ${workingDir}`
112
+ );
113
113
  }
114
- this.filename = /\.json$/.test(resolvedPath) ? resolvedPath : `${resolvedPath}.json`;
114
+ this.filename = /\.json$/.test(resolvedPath)
115
+ ? resolvedPath
116
+ : `${resolvedPath}.json`;
115
117
 
116
118
  // --- Security Check: Encryption Key ---
117
- if (options.encryptionKey && (!options.encryptionKey || Buffer.from(options.encryptionKey, 'hex').length !== 32)) {
118
- throw new SecurityError('Encryption key must be a 32-byte (64-character hex) string.');
119
+ if (
120
+ options.encryptionKey &&
121
+ (!options.encryptionKey ||
122
+ Buffer.from(options.encryptionKey, "hex").length !== 32)
123
+ ) {
124
+ throw new SecurityError(
125
+ "Encryption key must be a 32-byte (64-character hex) string."
126
+ );
119
127
  }
120
128
 
121
129
  this.config = {
@@ -123,7 +131,9 @@ class JSONDatabase extends EventEmitter {
123
131
  writeOnChange: options.writeOnChange !== false,
124
132
  schema: options.schema || null,
125
133
  indices: options.indices || [],
126
- encryptionKey: options.encryptionKey ? Buffer.from(options.encryptionKey, 'hex') : null,
134
+ encryptionKey: options.encryptionKey
135
+ ? Buffer.from(options.encryptionKey, "hex")
136
+ : null,
127
137
  };
128
138
 
129
139
  this.cache = null;
@@ -138,29 +148,44 @@ class JSONDatabase extends EventEmitter {
138
148
  // --- Encryption & Decryption ---
139
149
  _encrypt(data) {
140
150
  const iv = crypto.randomBytes(IV_LENGTH);
141
- const cipher = crypto.createCipheriv(ALGORITHM, this.config.encryptionKey, iv);
151
+ const cipher = crypto.createCipheriv(
152
+ ALGORITHM,
153
+ this.config.encryptionKey,
154
+ iv
155
+ );
142
156
  const jsonString = JSON.stringify(data);
143
- const encrypted = Buffer.concat([cipher.update(jsonString, 'utf8'), cipher.final()]);
157
+ const encrypted = Buffer.concat([
158
+ cipher.update(jsonString, "utf8"),
159
+ cipher.final(),
160
+ ]);
144
161
  const authTag = cipher.getAuthTag();
145
162
  return JSON.stringify({
146
- iv: iv.toString('hex'),
147
- tag: authTag.toString('hex'),
148
- content: encrypted.toString('hex'),
163
+ iv: iv.toString("hex"),
164
+ tag: authTag.toString("hex"),
165
+ content: encrypted.toString("hex"),
149
166
  });
150
167
  }
151
168
 
152
169
  _decrypt(encryptedPayload) {
153
170
  try {
154
171
  const payload = JSON.parse(encryptedPayload);
155
- const iv = Buffer.from(payload.iv, 'hex');
156
- const authTag = Buffer.from(payload.tag, 'hex');
157
- const encryptedContent = Buffer.from(payload.content, 'hex');
158
- const decipher = crypto.createDecipheriv(ALGORITHM, this.config.encryptionKey, iv);
172
+ const iv = Buffer.from(payload.iv, "hex");
173
+ const authTag = Buffer.from(payload.tag, "hex");
174
+ const encryptedContent = Buffer.from(payload.content, "hex");
175
+ const decipher = crypto.createDecipheriv(
176
+ ALGORITHM,
177
+ this.config.encryptionKey,
178
+ iv
179
+ );
159
180
  decipher.setAuthTag(authTag);
160
- const decrypted = decipher.update(encryptedContent, 'hex', 'utf8') + decipher.final('utf8');
181
+ const decrypted =
182
+ decipher.update(encryptedContent, "hex", "utf8") +
183
+ decipher.final("utf8");
161
184
  return JSON.parse(decrypted);
162
185
  } catch (e) {
163
- throw new SecurityError('Decryption failed. The file may be corrupted, tampered with, or the encryption key is incorrect.');
186
+ throw new SecurityError(
187
+ "Decryption failed. The file may be corrupted, tampered with, or the encryption key is incorrect."
188
+ );
164
189
  }
165
190
  }
166
191
 
@@ -168,13 +193,35 @@ class JSONDatabase extends EventEmitter {
168
193
 
169
194
  /** @private Kicks off the initialization process. */
170
195
  async _initialize() {
196
+ // --- FIX: Crash Recovery for Durable Writes ---
197
+ // Check if a temporary file exists from a previously failed write.
198
+ // If so, it represents the most recent state. We recover by renaming it.
199
+ const tempFile = this.filename + ".tmp";
200
+ try {
201
+ await fs.access(tempFile);
202
+ console.warn(
203
+ `[JSONDatabase] Found temporary file ${tempFile}. Recovering from a previous failed write.`
204
+ );
205
+ await fs.rename(tempFile, this.filename);
206
+ console.log(
207
+ `[JSONDatabase] Recovery successful. ${this.filename} has been restored.`
208
+ );
209
+ } catch (e) {
210
+ // This is the normal case where no temp file exists. Do nothing.
211
+ }
212
+
171
213
  try {
172
214
  await this._refreshCache();
173
215
  this._rebuildAllIndices();
174
216
  } catch (err) {
175
- const initError = new DBInitializationError(`Failed to initialize database: ${err.message}`);
176
- this.emit('error', initError);
177
- console.error(`[JSONDatabase] FATAL: Initialization failed for ${this.filename}. The database is in an unusable state.`, err);
217
+ const initError = new DBInitializationError(
218
+ `Failed to initialize database: ${err.message}`
219
+ );
220
+ this.emit("error", initError);
221
+ console.error(
222
+ `[JSONDatabase] FATAL: Initialization failed for ${this.filename}. The database is in an unusable state.`,
223
+ err
224
+ );
178
225
  // --- ENHANCEMENT: Make the instance unusable if init fails ---
179
226
  // By re-throwing here, the _initPromise will be rejected, and all subsequent
180
227
  // operations waiting on _ensureInitialized() will fail immediately.
@@ -185,22 +232,25 @@ class JSONDatabase extends EventEmitter {
185
232
  /** @private Reads file, decrypts if necessary, and populates cache. */
186
233
  async _refreshCache() {
187
234
  try {
188
- const fileContent = await fs.readFile(this.filename, 'utf8');
235
+ const fileContent = await fs.readFile(this.filename, "utf8");
189
236
  if (this.config.encryptionKey) {
190
- this.cache = fileContent.trim() === '' ? {} : this._decrypt(fileContent);
237
+ this.cache =
238
+ fileContent.trim() === "" ? {} : this._decrypt(fileContent);
191
239
  } else {
192
- this.cache = fileContent.trim() === '' ? {} : JSON.parse(fileContent);
240
+ this.cache = fileContent.trim() === "" ? {} : JSON.parse(fileContent);
193
241
  }
194
242
  this.stats.reads++;
195
243
  } catch (err) {
196
- if (err.code === 'ENOENT') {
197
- console.warn(`[JSONDatabase] File ${this.filename} not found. Creating.`);
244
+ if (err.code === "ENOENT") {
245
+ console.warn(
246
+ `[JSONDatabase] File ${this.filename} not found. Creating.`
247
+ );
198
248
  this.cache = {};
199
- const initialContent = this.config.encryptionKey ? this._encrypt({}) : '{}';
200
- await fs.writeFile(this.filename, initialContent, 'utf8');
201
- this.stats.writes++;
249
+ // Do not write file here; _atomicWrite will create it safely.
202
250
  } else if (err instanceof SyntaxError && !this.config.encryptionKey) {
203
- throw new DBInitializationError(`Failed to parse JSON from ${this.filename}. File is corrupted.`);
251
+ throw new DBInitializationError(
252
+ `Failed to parse JSON from ${this.filename}. File is corrupted.`
253
+ );
204
254
  } else {
205
255
  throw err; // Re-throw security, crypto, and other errors
206
256
  }
@@ -209,41 +259,56 @@ class JSONDatabase extends EventEmitter {
209
259
 
210
260
  /** @private Ensures all operations wait for initialization to complete. */
211
261
  async _ensureInitialized() {
212
- // This promise will be rejected if _initialize() fails, stopping all operations.
213
- return this._initPromise;
262
+ // This promise will be rejected if _initialize() fails, stopping all operations.
263
+ return this._initPromise;
214
264
  }
215
265
 
216
266
  /** @private Performs an atomic write operation. */
217
267
  async _atomicWrite(operationFn) {
218
268
  await this._ensureInitialized();
219
269
 
220
- // This promise chain ensures all writes happen one after another.
270
+ // This promise chain ensures all writes *from this process* happen one after another.
221
271
  this.writeLock = this.writeLock.then(async () => {
222
- // Use the live cache as the source of truth for the transaction.
223
- const oldData = this.cache;
224
- const dataToModify = _.cloneDeep(oldData);
225
-
272
+ let releaseLock;
226
273
  try {
227
- // --- CRITICAL FIX: Await the operation function in case it's async ---
274
+ // --- FIX: Acquire a cross-process lock to prevent race conditions.
275
+ // This will wait if another process (or this one) currently holds the lock.
276
+ releaseLock = await lockfile.lock(this.filename, {
277
+ stale: 7000, // Lock is considered stale after 7s
278
+ retries: {
279
+ retries: 5,
280
+ factor: 1.2,
281
+ minTimeout: 200,
282
+ },
283
+ });
284
+
285
+ // --- FIX: Refresh cache *after* acquiring the lock.
286
+ // This is critical to get the latest data if another process changed it.
287
+ await this._refreshCache();
288
+
289
+ const oldData = this.cache;
290
+ const dataToModify = _.cloneDeep(oldData);
291
+
228
292
  const newData = await operationFn(dataToModify);
229
293
 
230
- // --- ENHANCEMENT: Stricter check to prevent accidental data loss ---
231
294
  if (newData === undefined) {
232
- throw new TransactionError("Atomic operation function returned undefined. Aborting to prevent data loss. Did you forget to `return data`?");
295
+ throw new TransactionError(
296
+ "Atomic operation function returned undefined. Aborting to prevent data loss. Did you forget to `return data`?"
297
+ );
233
298
  }
234
299
 
235
300
  if (this.config.schema) {
236
301
  const validationResult = this.config.schema.safeParse(newData);
237
302
  if (!validationResult.success) {
238
- throw new ValidationError('Schema validation failed.', validationResult.error.issues);
303
+ throw new ValidationError(
304
+ "Schema validation failed.",
305
+ validationResult.error.issues
306
+ );
239
307
  }
240
308
  }
241
-
242
- // --- ENHANCEMENT: Update indices *before* the write to catch violations early ---
243
- // This will throw an IndexViolationError if there's a problem.
309
+
244
310
  this._updateIndices(oldData, newData);
245
311
 
246
- // Only write to disk if data has actually changed.
247
312
  if (this.config.writeOnChange && _.isEqual(newData, oldData)) {
248
313
  return oldData; // Return the unchanged data
249
314
  }
@@ -251,89 +316,114 @@ class JSONDatabase extends EventEmitter {
251
316
  const contentToWrite = this.config.encryptionKey
252
317
  ? this._encrypt(newData)
253
318
  : JSON.stringify(newData, null, this.config.prettyPrint ? 2 : 0);
254
-
255
- await fs.writeFile(this.filename, contentToWrite, 'utf8');
256
319
 
257
- // Update cache only after a successful write.
320
+ // --- FIX: Implement durable write. Write to temp file first.
321
+ const tempFile = this.filename + ".tmp";
322
+ await fs.writeFile(tempFile, contentToWrite, "utf8");
323
+ // --- FIX: Atomically rename temp file to the final filename.
324
+ await fs.rename(tempFile, this.filename);
325
+
258
326
  this.cache = newData;
259
327
  this.stats.writes++;
260
-
261
- this.emit('write', { filename: this.filename, timestamp: Date.now() });
262
- this.emit('change', { oldValue: oldData, newValue: newData });
263
328
 
264
- return newData;
329
+ this.emit("write", { filename: this.filename, timestamp: Date.now() });
330
+ this.emit("change", { oldValue: oldData, newValue: newData });
265
331
 
332
+ return newData;
266
333
  } catch (error) {
267
- // If any part of the transaction fails, emit the error and re-throw.
268
- // The cache remains unchanged from before the operation.
269
- this.emit('error', error);
270
- console.error("[JSONDatabase] Atomic write failed. No changes were saved.", error);
271
- throw error; // Propagate the error to the caller.
334
+ this.emit("error", error);
335
+ console.error(
336
+ "[JSONDatabase] Atomic write failed. No changes were saved.",
337
+ error
338
+ );
339
+ throw error;
340
+ } finally {
341
+ // --- FIX: Always release the lock, even if an error occurred.
342
+ if (releaseLock) {
343
+ await releaseLock();
344
+ }
272
345
  }
273
346
  });
274
347
 
275
348
  return this.writeLock;
276
349
  }
277
-
350
+
278
351
  // --- Indexing ---
279
352
 
280
353
  /** @private Clears and rebuilds all defined indices from the current cache. */
281
354
  _rebuildAllIndices() {
282
- this._indices.clear();
283
- for (const indexDef of this.config.indices) {
284
- this._indices.set(indexDef.name, new Map());
285
- }
286
- if (this.config.indices.length > 0 && !_.isEmpty(this.cache)) {
287
- this._updateIndices({}, this.cache); // Treat it as a full "add" operation
288
- }
289
- console.log(`[JSONDatabase] Rebuilt ${this.config.indices.length} indices for ${this.filename}.`);
355
+ this._indices.clear();
356
+ for (const indexDef of this.config.indices) {
357
+ this._indices.set(indexDef.name, new Map());
358
+ }
359
+ if (this.config.indices.length > 0 && !_.isEmpty(this.cache)) {
360
+ // Rebuild by treating the current state as "new" and the previous state as empty.
361
+ this._updateIndices({}, this.cache);
362
+ }
363
+ console.log(
364
+ `[JSONDatabase] Rebuilt ${this.config.indices.length} indices for ${this.filename}.`
365
+ );
290
366
  }
291
367
 
292
- /** @private Compares old and new data to update indices efficiently. */
368
+ /**
369
+ * @private Compares old and new data to update indices efficiently.
370
+ * FIX: Replaced inefficient and buggy index update logic with a robust key-based comparison.
371
+ * This new implementation correctly handles additions, deletions, and in-place updates,
372
+ * and is significantly more performant.
373
+ */
293
374
  _updateIndices(oldData, newData) {
294
- for (const indexDef of this.config.indices) {
295
- const collectionPath = indexDef.path;
296
- const field = indexDef.field;
297
- const indexMap = this._indices.get(indexDef.name);
298
-
299
- const oldCollection = _.get(oldData, collectionPath, []);
300
- const newCollection = _.get(newData, collectionPath, []);
301
-
302
- // This logic works for both arrays of objects and objects of objects (maps)
303
- const oldItems = _.values(oldCollection);
304
- const newItems = _.values(newCollection);
305
-
306
- const oldMap = new Map(oldItems.map(item => [item[field], item]));
307
- const newMap = new Map(newItems.map(item => [item[field], item]));
308
-
309
- // Find values that were removed or changed
310
- for (const [oldValue, oldItem] of oldMap.entries()) {
311
- if (oldValue !== undefined && !newMap.has(oldValue)) {
312
- indexMap.delete(oldValue);
313
- }
314
- }
315
-
316
- // Find values that were added or changed
317
- for (const [newValue, newItem] of newMap.entries()) {
318
- if (newValue !== undefined && !oldMap.has(newValue)) {
319
- if (indexDef.unique && indexMap.has(newValue)) {
320
- throw new IndexViolationError(`Unique index '${indexDef.name}' violated for value '${newValue}'.`);
321
- }
322
- // To find the key, we need to iterate, which isn't ideal but necessary here.
323
- const key = _.findKey(newCollection, {[field]: newValue});
324
- indexMap.set(newValue, key);
325
- }
375
+ for (const indexDef of this.config.indices) {
376
+ const indexMap = this._indices.get(indexDef.name);
377
+ if (!indexMap) continue;
378
+
379
+ const oldCollection = _.get(oldData, indexDef.path, {});
380
+ const newCollection = _.get(newData, indexDef.path, {});
381
+
382
+ if (!_.isObject(oldCollection) || !_.isObject(newCollection)) {
383
+ continue; // Indexing requires a collection (object or array).
384
+ }
385
+
386
+ const allKeys = _.union(_.keys(oldCollection), _.keys(newCollection));
387
+
388
+ for (const key of allKeys) {
389
+ const oldItem = oldCollection[key];
390
+ const newItem = newCollection[key];
391
+
392
+ if (_.isEqual(oldItem, newItem)) {
393
+ continue; // Item is unchanged, no index update needed.
394
+ }
395
+
396
+ const oldVal = oldItem?.[indexDef.field];
397
+ const newVal = newItem?.[indexDef.field];
398
+
399
+ if (_.isEqual(oldVal, newVal)) {
400
+ continue; // Indexed field's value is unchanged.
401
+ }
402
+
403
+ // 1. Remove the old value if it was indexed and pointed to this item.
404
+ if (oldVal !== undefined && indexMap.get(oldVal) === key) {
405
+ indexMap.delete(oldVal);
406
+ }
407
+
408
+ // 2. Add the new value if it's defined.
409
+ if (newVal !== undefined) {
410
+ // Check for unique constraint violation before adding.
411
+ if (indexDef.unique && indexMap.has(newVal)) {
412
+ throw new IndexViolationError(
413
+ `Unique index '${indexDef.name}' violated for value '${newVal}'.`
414
+ );
326
415
  }
416
+ indexMap.set(newVal, key);
417
+ }
327
418
  }
419
+ }
328
420
  }
329
421
 
330
-
331
422
  // --- Public API ---
332
423
 
333
424
  async get(path, defaultValue) {
334
425
  await this._ensureInitialized();
335
426
  this.stats.cacheHits++;
336
- // --- CRITICAL FIX: Handle undefined/null path to get the entire object ---
337
427
  if (path === undefined || path === null) {
338
428
  return this.cache;
339
429
  }
@@ -347,7 +437,7 @@ class JSONDatabase extends EventEmitter {
347
437
  }
348
438
 
349
439
  async set(path, value) {
350
- return this._atomicWrite(data => {
440
+ return this._atomicWrite((data) => {
351
441
  _.set(data, path, value);
352
442
  return data;
353
443
  });
@@ -355,7 +445,7 @@ class JSONDatabase extends EventEmitter {
355
445
 
356
446
  async delete(path) {
357
447
  let deleted = false;
358
- await this._atomicWrite(data => {
448
+ await this._atomicWrite((data) => {
359
449
  deleted = _.unset(data, path);
360
450
  return data;
361
451
  });
@@ -364,12 +454,11 @@ class JSONDatabase extends EventEmitter {
364
454
 
365
455
  async push(path, ...items) {
366
456
  if (items.length === 0) return;
367
- return this._atomicWrite(data => {
457
+ return this._atomicWrite((data) => {
368
458
  const arr = _.get(data, path);
369
459
  const targetArray = Array.isArray(arr) ? arr : [];
370
- items.forEach(item => {
371
- // Use deep comparison to ensure object uniqueness
372
- if (!targetArray.some(existing => _.isEqual(existing, item))) {
460
+ items.forEach((item) => {
461
+ if (!targetArray.some((existing) => _.isEqual(existing, item))) {
373
462
  targetArray.push(item);
374
463
  }
375
464
  });
@@ -380,7 +469,7 @@ class JSONDatabase extends EventEmitter {
380
469
 
381
470
  async pull(path, ...itemsToRemove) {
382
471
  if (itemsToRemove.length === 0) return;
383
- return this._atomicWrite(data => {
472
+ return this._atomicWrite((data) => {
384
473
  const arr = _.get(data, path);
385
474
  if (Array.isArray(arr)) {
386
475
  _.pullAllWith(arr, itemsToRemove, _.isEqual);
@@ -396,43 +485,49 @@ class JSONDatabase extends EventEmitter {
396
485
  async batch(ops, options = { stopOnError: false }) {
397
486
  if (!Array.isArray(ops) || ops.length === 0) return;
398
487
 
399
- return this._atomicWrite(data => {
488
+ return this._atomicWrite((data) => {
400
489
  for (const [index, op] of ops.entries()) {
401
490
  try {
402
- if (!op || !op.type || op.path === undefined) throw new Error("Invalid operation format: missing type or path.");
403
-
491
+ if (!op || !op.type || op.path === undefined)
492
+ throw new Error("Invalid operation format: missing type or path.");
493
+
404
494
  switch (op.type) {
405
- case 'set':
406
- if (!op.hasOwnProperty('value')) throw new Error("Set operation missing 'value'.");
495
+ case "set":
496
+ if (!op.hasOwnProperty("value"))
497
+ throw new Error("Set operation missing 'value'.");
407
498
  _.set(data, op.path, op.value);
408
499
  break;
409
- case 'delete':
500
+ case "delete":
410
501
  _.unset(data, op.path);
411
502
  break;
412
- case 'push':
413
- if (!Array.isArray(op.values)) throw new Error("Push operation 'values' must be an array.");
503
+ case "push":
504
+ if (!Array.isArray(op.values))
505
+ throw new Error("Push operation 'values' must be an array.");
414
506
  const arr = _.get(data, op.path);
415
507
  const targetArray = Array.isArray(arr) ? arr : [];
416
- op.values.forEach(item => {
417
- if (!targetArray.some(existing => _.isEqual(existing, item))) targetArray.push(item);
508
+ op.values.forEach((item) => {
509
+ if (!targetArray.some((existing) => _.isEqual(existing, item)))
510
+ targetArray.push(item);
418
511
  });
419
512
  _.set(data, op.path, targetArray);
420
513
  break;
421
- case 'pull':
422
- if (!Array.isArray(op.values)) throw new Error("Pull operation 'values' must be an array.");
514
+ case "pull":
515
+ if (!Array.isArray(op.values))
516
+ throw new Error("Pull operation 'values' must be an array.");
423
517
  const pullArr = _.get(data, op.path);
424
- if (Array.isArray(pullArr)) _.pullAllWith(pullArr, op.values, _.isEqual);
518
+ if (Array.isArray(pullArr))
519
+ _.pullAllWith(pullArr, op.values, _.isEqual);
425
520
  break;
426
521
  default:
427
522
  throw new Error(`Unsupported operation type: '${op.type}'.`);
428
523
  }
429
524
  } catch (err) {
430
- const errorMessage = `[JSONDatabase] Batch failed at operation index ${index} (type: ${op?.type}): ${err.message}`;
431
- if (options.stopOnError) {
432
- throw new Error(errorMessage);
433
- } else {
434
- console.error(errorMessage);
435
- }
525
+ const errorMessage = `[JSONDatabase] Batch failed at operation index ${index} (type: ${op?.type}): ${err.message}`;
526
+ if (options.stopOnError) {
527
+ throw new Error(errorMessage);
528
+ } else {
529
+ console.error(errorMessage);
530
+ }
436
531
  }
437
532
  }
438
533
  return data;
@@ -440,35 +535,35 @@ class JSONDatabase extends EventEmitter {
440
535
  }
441
536
 
442
537
  async find(collectionPath, predicate) {
443
- await this._ensureInitialized();
444
- const collection = _.get(this.cache, collectionPath);
445
- // Works for both objects and arrays
446
- if (typeof collection !== 'object' || collection === null) return undefined;
447
-
448
- this.stats.cacheHits++;
449
- return _.find(collection, predicate);
538
+ await this._ensureInitialized();
539
+ const collection = _.get(this.cache, collectionPath);
540
+ if (typeof collection !== "object" || collection === null) return undefined;
541
+
542
+ this.stats.cacheHits++;
543
+ return _.find(collection, predicate);
450
544
  }
451
-
545
+
452
546
  async findByIndex(indexName, value) {
453
- await this._ensureInitialized();
454
- if (!this._indices.has(indexName)) {
455
- throw new Error(`Index with name '${indexName}' does not exist.`);
456
- }
547
+ await this._ensureInitialized();
548
+ if (!this._indices.has(indexName)) {
549
+ throw new Error(`Index with name '${indexName}' does not exist.`);
550
+ }
457
551
 
458
- this.stats.cacheHits++;
459
- const indexMap = this._indices.get(indexName);
460
- const objectKey = indexMap.get(value);
552
+ this.stats.cacheHits++;
553
+ const indexMap = this._indices.get(indexName);
554
+ const objectKey = indexMap.get(value);
461
555
 
462
- if (objectKey === undefined) return undefined;
556
+ if (objectKey === undefined) return undefined;
463
557
 
464
- const indexDef = this.config.indices.find(i => i.name === indexName);
465
- // Construct the full path to the object
466
- const fullPath = [..._.toPath(indexDef.path), objectKey];
467
- return _.get(this.cache, fullPath);
558
+ const indexDef = this.config.indices.find((i) => i.name === indexName);
559
+ const fullPath = [..._.toPath(indexDef.path), objectKey];
560
+ return _.get(this.cache, fullPath);
468
561
  }
469
562
 
470
563
  async clear() {
471
- console.warn(`[JSONDatabase] Clearing all data from ${this.filename}. This action is irreversible.`);
564
+ console.warn(
565
+ `[JSONDatabase] Clearing all data from ${this.filename}. This action is irreversible.`
566
+ );
472
567
  return this._atomicWrite(() => ({}));
473
568
  }
474
569
 
@@ -477,17 +572,18 @@ class JSONDatabase extends EventEmitter {
477
572
  }
478
573
 
479
574
  async close() {
480
- // Wait for the last pending write operation to finish
481
575
  await this.writeLock;
482
-
576
+
483
577
  this.cache = null;
484
578
  this._indices.clear();
485
579
  this.removeAllListeners();
486
- this._initPromise = null; // Allow for garbage collection
580
+ this._initPromise = null;
487
581
 
488
582
  const finalStats = JSON.stringify(this.getStats());
489
- console.log(`[JSONDatabase] Closed connection to ${this.filename}. Final Stats: ${finalStats}`);
583
+ console.log(
584
+ `[JSONDatabase] Closed connection to ${this.filename}. Final Stats: ${finalStats}`
585
+ );
490
586
  }
491
587
  }
492
588
 
493
- module.exports = JSONDatabase;
589
+ module.exports = JSONDatabase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-database-st",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "A simple, promise-based JSON file database for Node.js with atomic operations and lodash integration.",
5
5
  "main": "JSONDatabase.js",
6
6
  "scripts": {
@@ -31,7 +31,8 @@
31
31
  "homepage": "https://github.com/sethunthunder111/json-database-st#readme",
32
32
  "dependencies": {
33
33
  "json-database-st": "^1.0.0",
34
- "lodash": "^4.17.21"
34
+ "lodash": "^4.17.21",
35
+ "proper-lockfile": "^4.1.2"
35
36
  },
36
37
  "engines": {
37
38
  "node": ">=14.0.0"