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.
- package/JSONDatabase.js +55 -53
- 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
|
-
|
|
290
|
-
const
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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