json-database-st 1.0.7 → 1.0.8

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 +55 -53
  2. package/package.json +1 -1
package/JSONDatabase.js CHANGED
@@ -175,6 +175,9 @@ class JSONDatabase extends EventEmitter {
175
175
  const initError = new DBInitializationError(`Failed to initialize database: ${err.message}`);
176
176
  this.emit('error', initError);
177
177
  console.error(`[JSONDatabase] FATAL: Initialization failed for ${this.filename}. The database is in an unusable state.`, err);
178
+ // --- ENHANCEMENT: Make the instance unusable if init fails ---
179
+ // By re-throwing here, the _initPromise will be rejected, and all subsequent
180
+ // operations waiting on _ensureInitialized() will fail immediately.
178
181
  throw initError;
179
182
  }
180
183
  }
@@ -206,6 +209,7 @@ class JSONDatabase extends EventEmitter {
206
209
 
207
210
  /** @private Ensures all operations wait for initialization to complete. */
208
211
  async _ensureInitialized() {
212
+ // This promise will be rejected if _initialize() fails, stopping all operations.
209
213
  return this._initPromise;
210
214
  }
211
215
 
@@ -213,16 +217,19 @@ class JSONDatabase extends EventEmitter {
213
217
  async _atomicWrite(operationFn) {
214
218
  await this._ensureInitialized();
215
219
 
220
+ // This promise chain ensures all writes happen one after another.
216
221
  this.writeLock = this.writeLock.then(async () => {
222
+ // Use the live cache as the source of truth for the transaction.
217
223
  const oldData = this.cache;
218
224
  const dataToModify = _.cloneDeep(oldData);
219
225
 
220
226
  try {
221
- // --- FIX: Await the operation function in case it's async ---
227
+ // --- CRITICAL FIX: Await the operation function in case it's async ---
222
228
  const newData = await operationFn(dataToModify);
223
229
 
230
+ // --- ENHANCEMENT: Stricter check to prevent accidental data loss ---
224
231
  if (newData === undefined) {
225
- throw new TransactionError("Atomic operation function returned undefined. Aborting to prevent data loss.");
232
+ throw new TransactionError("Atomic operation function returned undefined. Aborting to prevent data loss. Did you forget to `return data`?");
226
233
  }
227
234
 
228
235
  if (this.config.schema) {
@@ -231,12 +238,15 @@ class JSONDatabase extends EventEmitter {
231
238
  throw new ValidationError('Schema validation failed.', validationResult.error.issues);
232
239
  }
233
240
  }
241
+
242
+ // --- ENHANCEMENT: Update indices *before* the write to catch violations early ---
243
+ // This will throw an IndexViolationError if there's a problem.
244
+ this._updateIndices(oldData, newData);
234
245
 
246
+ // Only write to disk if data has actually changed.
235
247
  if (this.config.writeOnChange && _.isEqual(newData, oldData)) {
236
- return oldData;
248
+ return oldData; // Return the unchanged data
237
249
  }
238
-
239
- this._updateIndices(oldData, newData);
240
250
 
241
251
  const contentToWrite = this.config.encryptionKey
242
252
  ? this._encrypt(newData)
@@ -244,6 +254,7 @@ class JSONDatabase extends EventEmitter {
244
254
 
245
255
  await fs.writeFile(this.filename, contentToWrite, 'utf8');
246
256
 
257
+ // Update cache only after a successful write.
247
258
  this.cache = newData;
248
259
  this.stats.writes++;
249
260
 
@@ -253,9 +264,11 @@ class JSONDatabase extends EventEmitter {
253
264
  return newData;
254
265
 
255
266
  } 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.
256
269
  this.emit('error', error);
257
270
  console.error("[JSONDatabase] Atomic write failed. No changes were saved.", error);
258
- throw error;
271
+ throw error; // Propagate the error to the caller.
259
272
  }
260
273
  });
261
274
 
@@ -283,49 +296,33 @@ class JSONDatabase extends EventEmitter {
283
296
  const field = indexDef.field;
284
297
  const indexMap = this._indices.get(indexDef.name);
285
298
 
286
- const oldCollection = _.get(oldData, collectionPath, {});
287
- const newCollection = _.get(newData, collectionPath, {});
299
+ const oldCollection = _.get(oldData, collectionPath, []);
300
+ const newCollection = _.get(newData, collectionPath, []);
288
301
 
289
- const oldKeys = Object.keys(oldCollection);
290
- const newKeys = Object.keys(newCollection);
291
-
292
- const addedKeys = _.difference(newKeys, oldKeys);
293
- const removedKeys = _.difference(oldKeys, newKeys);
294
- const potentiallyModifiedKeys = _.intersection(oldKeys, newKeys);
295
-
296
- for (const key of removedKeys) {
297
- const oldItem = oldCollection[key];
298
- if (oldItem && oldItem[field] !== undefined) {
299
- indexMap.delete(oldItem[field]);
300
- }
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);
302
305
 
303
- for (const key of addedKeys) {
304
- const newItem = newCollection[key];
305
- const indexValue = newItem?.[field];
306
- if (indexValue !== undefined) {
307
- if (indexDef.unique && indexMap.has(indexValue)) {
308
- throw new IndexViolationError(`Unique index '${indexDef.name}' violated for value '${indexValue}'.`);
309
- }
310
- indexMap.set(indexValue, key);
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);
311
313
  }
312
314
  }
313
-
314
- for (const key of potentiallyModifiedKeys) {
315
- const oldItem = oldCollection[key];
316
- const newItem = newCollection[key];
317
- const oldIndexValue = oldItem?.[field];
318
- const newIndexValue = newItem?.[field];
319
-
320
- if (!_.isEqual(oldItem, newItem) && oldIndexValue !== newIndexValue) {
321
- if (oldIndexValue !== undefined) indexMap.delete(oldIndexValue);
322
- if (newIndexValue !== undefined) {
323
- if (indexDef.unique && indexMap.has(newIndexValue)) {
324
- throw new IndexViolationError(`Unique index '${indexDef.name}' violated for value '${newIndexValue}'.`);
325
- }
326
- indexMap.set(newIndexValue, key);
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}'.`);
327
321
  }
328
- }
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
+ }
329
326
  }
330
327
  }
331
328
  }
@@ -336,7 +333,7 @@ class JSONDatabase extends EventEmitter {
336
333
  async get(path, defaultValue) {
337
334
  await this._ensureInitialized();
338
335
  this.stats.cacheHits++;
339
- // --- FIX: Handle undefined/null path to get the entire object ---
336
+ // --- CRITICAL FIX: Handle undefined/null path to get the entire object ---
340
337
  if (path === undefined || path === null) {
341
338
  return this.cache;
342
339
  }
@@ -350,7 +347,7 @@ class JSONDatabase extends EventEmitter {
350
347
  }
351
348
 
352
349
  async set(path, value) {
353
- await this._atomicWrite(data => {
350
+ return this._atomicWrite(data => {
354
351
  _.set(data, path, value);
355
352
  return data;
356
353
  });
@@ -367,10 +364,11 @@ class JSONDatabase extends EventEmitter {
367
364
 
368
365
  async push(path, ...items) {
369
366
  if (items.length === 0) return;
370
- await this._atomicWrite(data => {
367
+ return this._atomicWrite(data => {
371
368
  const arr = _.get(data, path);
372
369
  const targetArray = Array.isArray(arr) ? arr : [];
373
370
  items.forEach(item => {
371
+ // Use deep comparison to ensure object uniqueness
374
372
  if (!targetArray.some(existing => _.isEqual(existing, item))) {
375
373
  targetArray.push(item);
376
374
  }
@@ -382,7 +380,7 @@ class JSONDatabase extends EventEmitter {
382
380
 
383
381
  async pull(path, ...itemsToRemove) {
384
382
  if (itemsToRemove.length === 0) return;
385
- await this._atomicWrite(data => {
383
+ return this._atomicWrite(data => {
386
384
  const arr = _.get(data, path);
387
385
  if (Array.isArray(arr)) {
388
386
  _.pullAllWith(arr, itemsToRemove, _.isEqual);
@@ -398,7 +396,7 @@ class JSONDatabase extends EventEmitter {
398
396
  async batch(ops, options = { stopOnError: false }) {
399
397
  if (!Array.isArray(ops) || ops.length === 0) return;
400
398
 
401
- await this._atomicWrite(data => {
399
+ return this._atomicWrite(data => {
402
400
  for (const [index, op] of ops.entries()) {
403
401
  try {
404
402
  if (!op || !op.type || op.path === undefined) throw new Error("Invalid operation format: missing type or path.");
@@ -444,6 +442,7 @@ class JSONDatabase extends EventEmitter {
444
442
  async find(collectionPath, predicate) {
445
443
  await this._ensureInitialized();
446
444
  const collection = _.get(this.cache, collectionPath);
445
+ // Works for both objects and arrays
447
446
  if (typeof collection !== 'object' || collection === null) return undefined;
448
447
 
449
448
  this.stats.cacheHits++;
@@ -463,12 +462,14 @@ class JSONDatabase extends EventEmitter {
463
462
  if (objectKey === undefined) return undefined;
464
463
 
465
464
  const indexDef = this.config.indices.find(i => i.name === indexName);
466
- return _.get(this.cache, [..._.toPath(indexDef.path), objectKey]);
465
+ // Construct the full path to the object
466
+ const fullPath = [..._.toPath(indexDef.path), objectKey];
467
+ return _.get(this.cache, fullPath);
467
468
  }
468
469
 
469
470
  async clear() {
470
- console.warn(`[JSONDatabase] Clearing all data from ${this.filename}.`);
471
- await this._atomicWrite(() => ({}));
471
+ console.warn(`[JSONDatabase] Clearing all data from ${this.filename}. This action is irreversible.`);
472
+ return this._atomicWrite(() => ({}));
472
473
  }
473
474
 
474
475
  getStats() {
@@ -476,12 +477,13 @@ class JSONDatabase extends EventEmitter {
476
477
  }
477
478
 
478
479
  async close() {
480
+ // Wait for the last pending write operation to finish
479
481
  await this.writeLock;
480
482
 
481
483
  this.cache = null;
482
484
  this._indices.clear();
483
485
  this.removeAllListeners();
484
- this._initPromise = null;
486
+ this._initPromise = null; // Allow for garbage collection
485
487
 
486
488
  const finalStats = JSON.stringify(this.getStats());
487
489
  console.log(`[JSONDatabase] Closed connection to ${this.filename}. Final Stats: ${finalStats}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-database-st",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
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": {