diff --git a/CHANGELOG.md b/CHANGELOG.md index 548caf1..96f5a16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2025-07-07 + +### Added +- **Unique Fields Support** - Prevent duplicate values in specified fields + - Configure unique fields via `uniqueFields` option + - Automatic validation on create and update operations + - Clear error messages for duplicate field values +- **Auto-ID Toggle** - Control automatic ID assignment + - Enable/disable auto-ID with `autoId` option + - Default behavior remains unchanged (auto-ID enabled) +- **deleteAll Method** - Remove all items from the database + - Simple `deleteAll(callback)` method + - Thread-safe operation through existing queue system +- **createCrud Convenience Function** - Quick CRUD instance creation + - Simplified API: `createCrud(filePath, options)` + - Exported as named export for easy access +- **Automatic Directory Creation** - Create directories if they don't exist + - Automatically creates parent directories for file paths + - No need to manually create directories before using the library + +### Enhanced +- **Test Suite Reorganization** - Improved test structure + - Split tests into logical files by functionality + - `test-basic.js` - Basic functionality and convenience features + - `test-config-options.js` - Configuration options (uniqueFields, autoId) + - `test-delete.js` - Delete operations including deleteAll + - Total test count increased to 37 tests +- **Configuration Options** - Enhanced constructor options + - `uniqueFields: string[]` - Array of field names that must be unique + - `autoId: boolean` - Enable/disable automatic ID assignment + - Backward compatible with existing code + +### Changed +- Package description updated to reflect new features +- Test scripts updated for reorganized test structure + +### Technical Details +- All new features maintain backward compatibility +- Thread-safe operations through existing queue system +- Comprehensive error handling for all new features +- Zero breaking changes to existing API + ## [1.0.0] - 2025-07-07 ### Added @@ -55,3 +97,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Batch operations (createMany, updateMany, deleteMany) - File locking for multi-process safety - Enhanced documentation and examples + +--- + +[1.1.0]: https://github.com/arielweizman/json-file-crud/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/arielweizman/json-file-crud/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 05f4207..7230945 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,13 @@ A simple, robust, and thread-safe CRUD library for managing JSON objects in file - **Simple API** - Easy to use CRUD operations - **Thread-safe** - Sequential operations with automatic queuing -- **Auto-ID assignment** - Automatic ID generation for new items -- **Configurable ID field** - Use any field name as the primary key -- **Comprehensive error handling** - Detailed error messages and validation +- **Auto-ID assignment** - Automatic ID generation for new items (configurable) +- **Unique Fields** - Prevent duplicate values in specified fields ✨ *New in v1.1* +- **Concurrent Operations** - Thread-safe operations with automatic queuing +- **Custom ID Fields** - Use any field name as the primary key (default: 'id') +- **Directory Creation** - Automatically creates directories if they don't exist ✨ *New in v1.1* +- **Convenience Functions** - Helper functions for quick setup ✨ *New in v1.1* +- **Error Handling** - Comprehensive error handling and detailed error messages - **Zero dependencies** - Built with only Node.js built-in modules - **ESM support** - Full ES modules support @@ -21,10 +25,20 @@ npm install json-file-crud ## Quick Start ```javascript -import JsonFileCRUD from 'json-file-crud'; +import JsonFileCRUD, { createCrud } from 'json-file-crud'; +// Standard usage const db = new JsonFileCRUD('./data.json'); +// Quick setup with convenience function +const db2 = createCrud('./users.json'); + +// Advanced configuration with unique fields +const userDb = new JsonFileCRUD('./users.json', { + uniqueFields: ['email', 'username'], + autoId: true +}); + // Create a new item db.create({ name: 'John', age: 30 }, (err, result) => { if (err) { @@ -67,16 +81,41 @@ db.delete(1, (err, deletedItem) => { Creates a new JsonFileCRUD instance. -- `filePath` (string): Path to the JSON file +- `filePath` (string): Path to the JSON file (directories will be created if they don't exist) - `options` (object, optional): - `idField` (string): Name of the ID field (default: 'id') + - `uniqueFields` (array): Array of field names that must be unique (default: []) + - `autoId` (boolean): Enable automatic ID assignment (default: true) ```javascript -// Default ID field +// Default settings const db = new JsonFileCRUD('./data.json'); // Custom ID field const products = new JsonFileCRUD('./products.json', { idField: 'productId' }); + +// Unique fields validation +const users = new JsonFileCRUD('./users.json', { + uniqueFields: ['email', 'username'] +}); + +// Disable auto-ID +const manualDb = new JsonFileCRUD('./manual.json', { autoId: false }); + +// Deep directory path (automatically created) +const deepDb = new JsonFileCRUD('./data/nested/deep/file.json'); +``` + +### Convenience Functions + +#### `createCrud(filePath, options)` + +Quick way to create a JsonFileCRUD instance. + +```javascript +import { createCrud } from 'json-file-crud'; + +const db = createCrud('./data.json', { uniqueFields: ['email'] }); ``` ### CRUD Operations @@ -165,6 +204,19 @@ db.delete(1, (err, deleted) => { }); ``` +#### `deleteAll(callback)` + +Deletes all items from the database. + +- `callback` (function): `(error) => {}` + +```javascript +db.deleteAll((err) => { + if (err) throw err; + console.log('All items deleted'); +}); +``` + #### `count(callback)` Returns the total number of items. @@ -197,77 +249,82 @@ db.writeAll(newData, (err) => { ## Advanced Features -### Auto-ID Assignment +### Unique Fields -When creating items without an ID, JsonFileCRUD automatically assigns the next available numeric ID: +Prevent duplicate values in specified fields: ```javascript -db.create({ name: 'John' }, (err, result) => { - // result: { name: 'John', id: 1 } +const userDb = new JsonFileCRUD('./users.json', { + uniqueFields: ['email', 'username'] }); -db.create({ name: 'Jane' }, (err, result) => { - // result: { name: 'Jane', id: 2 } +// This will fail if email already exists +userDb.create({ + name: 'John', + email: 'john@example.com' +}, (err, user) => { + // err.message: "Item with email 'john@example.com' already exists" }); ``` -### Concurrent Operations +### Auto-ID Control -All write operations are automatically queued to prevent race conditions: +Disable automatic ID assignment: ```javascript -// These will be executed sequentially, not simultaneously -db.create({ name: 'User 1' }, callback1); -db.create({ name: 'User 2' }, callback2); -db.update(1, { active: true }, callback3); +const db = new JsonFileCRUD('./data.json', { autoId: false }); + +// No ID will be auto-generated +db.create({ name: 'Test' }, (err, item) => { + // item: { name: 'Test' } (no id field) +}); ``` -### Custom ID Fields +### Directory Creation -You can use any field name as the primary key: +Automatically creates directories for deep paths: ```javascript -const products = new JsonFileCRUD('./products.json', { idField: 'productId' }); - -products.create({ name: 'Laptop', price: 999 }, (err, product) => { - // product: { name: 'Laptop', price: 999, productId: 1 } -}); +// This will create ./data/users/ directories if they don't exist +const db = new JsonFileCRUD('./data/users/profiles.json'); ``` -### Error Handling +## Examples + +For comprehensive examples, see the [examples](./examples/) directory: + +- **[Basic Usage](./examples/basic-usage.js)** - Simple CRUD operations +- **[Advanced Features](./examples/advanced-usage.js)** - Concurrent operations, filtering, custom ID fields +- **[User Management](./examples/user-management.js)** - Real-world application with unique fields validation -JsonFileCRUD provides detailed error messages for common scenarios: +### Quick Examples ```javascript -// Validation errors -db.create(null, (err) => { - // err.message: "Item must be an object" +// Basic usage with unique fields +import JsonFileCRUD, { createCrud } from 'json-file-crud'; + +const userDb = createCrud('./users.json', { + uniqueFields: ['email', 'username'] }); -// Not found errors -db.findById(999, (err) => { - // err.message: "Item with id 999 not found" +// Delete all users +userDb.deleteAll((err) => { + console.log('All users deleted'); }); -// Duplicate ID errors -db.create({ id: 1, name: 'Duplicate' }, (err) => { - // err.message: "Item with id 1 already exists" +// Example with auto-ID disabled +const manualDb = new JsonFileCRUD('./manual.json', { autoId: false }); +manualDb.create({ name: 'Test' }, (err, item) => { + // item: { name: 'Test' } (no auto-generated ID) }); ``` -## Examples - -For comprehensive examples, see the [examples](./examples/) directory: - -- **[Basic Usage](./examples/basic-usage.js)** - Simple CRUD operations -- **[Advanced Features](./examples/advanced-usage.js)** - Concurrent operations, filtering, custom ID fields -- **[User Management](./examples/user-management.js)** - Real-world application example - To run examples: ```bash -cd examples -node basic-usage.js +npm run examples +# or individually: +node examples/basic-usage.js ``` ## TypeScript Support diff --git a/lib/constants.js b/lib/constants.js index 131845a..cf1ea9a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -7,6 +7,7 @@ export const OPERATION_TYPES = { CREATE: 'create', UPDATE: 'update', DELETE: 'delete', + DELETE_ALL: 'deleteAll', WRITE_ALL: 'writeAll' }; @@ -20,5 +21,7 @@ export const ERROR_MESSAGES = { }; export const DEFAULT_CONFIG = { - ID_FIELD: 'id' + ID_FIELD: 'id', + AUTO_ID: true, + UNIQUE_FIELDS: [] }; diff --git a/lib/file-operations.js b/lib/file-operations.js index 6fe804e..ef186f0 100644 --- a/lib/file-operations.js +++ b/lib/file-operations.js @@ -1,9 +1,10 @@ /** * File operations utilities for JsonFileCRUD - * @version 1.0.0 + * @version 1.1.0 */ import fs from 'fs'; +import path from 'path'; import { ERROR_MESSAGES } from './constants.js'; /** @@ -38,6 +39,14 @@ export function readAllFromFile(filePath, callback) { * @param {Function} callback - Called with (error) */ export function writeItemsToFile(filePath, items, callback) { - const content = JSON.stringify(items, null, 2); - fs.writeFile(filePath, content, callback); + // Ensure directory exists + const dir = path.dirname(filePath); + fs.mkdir(dir, { recursive: true }, (mkdirErr) => { + if (mkdirErr) { + return callback(mkdirErr); + } + + const content = JSON.stringify(items, null, 2); + fs.writeFile(filePath, content, callback); + }); } diff --git a/lib/json-file-crud.js b/lib/json-file-crud.js index 58c2fb1..939c668 100644 --- a/lib/json-file-crud.js +++ b/lib/json-file-crud.js @@ -24,6 +24,8 @@ class JsonFileCRUD { * @param {string} filePath - Path to the JSON file * @param {Object} options - Configuration options * @param {string} options.idField - Name of the ID field (default: 'id') + * @param {boolean} options.autoId - Enable auto-ID assignment (default: true) + * @param {string[]} options.uniqueFields - Array of field names that should be unique */ constructor(filePath, options = {}) { if (!filePath) { @@ -31,6 +33,8 @@ class JsonFileCRUD { } this.filePath = path.resolve(filePath); this.idField = options.idField || DEFAULT_CONFIG.ID_FIELD; + this.autoId = options.autoId !== false; // default to true + this.uniqueFields = options.uniqueFields || []; // Array of field names that should be unique this.queueManager = new QueueManager(this.filePath); } @@ -49,17 +53,31 @@ class JsonFileCRUD { } this.queueManager.queueOperation(OPERATION_TYPES.CREATE, { item }, callback, (items) => { - // Auto-assign ID if not provided - if (!item[this.idField]) { + // Auto-assign ID if enabled and not provided + if (this.autoId && !item[this.idField]) { item[this.idField] = this._generateNextId(items); } // Check for duplicate ID - const existingItem = items.find(existingItem => - existingItem[this.idField] === item[this.idField] - ); - if (existingItem) { - return callback(createDuplicateIdError(this.idField, item[this.idField])); + if (item[this.idField]) { + const existingItem = items.find(existingItem => + existingItem[this.idField] === item[this.idField] + ); + if (existingItem) { + return callback(createDuplicateIdError(this.idField, item[this.idField])); + } + } + + // Check for duplicates in unique fields + for (const fieldName of this.uniqueFields) { + if (item[fieldName] !== undefined && item[fieldName] !== null) { + const duplicate = items.find(existingItem => + existingItem[fieldName] === item[fieldName] + ); + if (duplicate) { + return callback(new Error(`Item with ${fieldName} '${item[fieldName]}' already exists`)); + } + } } // Add new item to array @@ -171,6 +189,18 @@ class JsonFileCRUD { return callback(createNotFoundError(this.idField, id)); } + // Check for duplicates in unique fields (exclude current item) + for (const fieldName of this.uniqueFields) { + if (data[fieldName] !== undefined && data[fieldName] !== null) { + const duplicate = items.find((existingItem, index) => + index !== itemIndex && existingItem[fieldName] === data[fieldName] + ); + if (duplicate) { + return callback(new Error(`Item with ${fieldName} '${data[fieldName]}' already exists`)); + } + } + } + // Update item (merge data with existing item) const updatedItem = { ...items[itemIndex], ...data }; items[itemIndex] = updatedItem; @@ -208,6 +238,18 @@ class JsonFileCRUD { //#endregion DELETE + //#region BULK OPERATIONS + + /** + * Delete all items from the JSON file + * @param {Function} callback - Called with (error) + */ + deleteAll(callback) { + this.writeAll([], callback); + } + + //#endregion BULK OPERATIONS + //#region UTILITY METHODS /** @@ -254,3 +296,13 @@ class JsonFileCRUD { } export default JsonFileCRUD; + +/** + * Convenience function to create a new JsonFileCRUD instance + * @param {string} filePath - Path to the JSON file + * @param {Object} options - Configuration options + * @returns {JsonFileCRUD} New JsonFileCRUD instance + */ +export function createCrud(filePath, options = {}) { + return new JsonFileCRUD(filePath, options); +} diff --git a/package.json b/package.json index 66fcc9c..fad19af 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { "name": "json-file-crud", - "version": "1.0.0", + "version": "1.1.0", "title": "JsonFileCRUD", - "description": "A simple, robust, and thread-safe CRUD library for managing JSON objects in files", + "description": "A simple, robust, and thread-safe CRUD library for managing JSON objects in files with unique fields, auto-ID, and advanced features", "main": "./lib/json-file-crud.js", "type": "module", "scripts": { "start": "node .", - "test": "node test/test-basic.js && node test/test-read.js && node test/test-create.js && node test/test-update.js && node test/test-delete.js && node test/test-advanced-create.js", + "test": "node test/test-basic.js && node test/test-read.js && node test/test-create.js && node test/test-update.js && node test/test-delete.js && node test/test-advanced-create.js && node test/test-config-options.js", "test:basic": "node test/test-basic.js", "test:read": "node test/test-read.js", "test:create": "node test/test-create.js", "test:update": "node test/test-update.js", "test:delete": "node test/test-delete.js", - "test:advanced-create": "node test/test-advanced-create.js", + "test:config-options": "node test/test-config-options.js", "examples": "node examples/basic-usage.js && node examples/advanced-usage.js && node examples/user-management.js", "examples:basic": "node examples/basic-usage.js", "examples:advanced": "node examples/advanced-usage.js", diff --git a/test/test-basic.js b/test/test-basic.js index a256ec9..7cfda05 100644 --- a/test/test-basic.js +++ b/test/test-basic.js @@ -1,4 +1,5 @@ -import JsonFileCRUD from '../lib/json-file-crud.js'; +import JsonFileCRUD, { createCrud } from '../lib/json-file-crud.js'; +import fs from 'fs'; let passed = 0; let total = 0; @@ -44,5 +45,43 @@ test('has required methods', () => { }); }); +// createCrud convenience function +test('createCrud convenience function works', () => { + const testFile = "./test-create-crud.json"; + + // Clean up + if (fs.existsSync(testFile)) { + fs.unlinkSync(testFile); + } + + const convenienceCrud = createCrud(testFile); + + if (!convenienceCrud || typeof convenienceCrud.create !== 'function') { + throw new Error('createCrud failed to create valid instance'); + } + + // Clean up + if (fs.existsSync(testFile)) { + fs.unlinkSync(testFile); + } +}); + +// Directory creation +test('automatically creates directories', () => { + const deepPath = "./test-deep/nested/directory/data.json"; + + // Create instance (should create directories) + const deepCrud = new JsonFileCRUD(deepPath); + + if (!deepCrud || !deepCrud.filePath.includes('test-deep')) { + // Clean up + fs.rmSync("./test-deep", { recursive: true, force: true }); + throw new Error('failed to create instance with deep path'); + } + + // Clean up + fs.rmSync("./test-deep", { recursive: true, force: true }); +}); + console.log(`\n${passed}/${total} tests passed`); process.exit(passed === total ? 0 : 1); diff --git a/test/test-config-options.js b/test/test-config-options.js new file mode 100644 index 0000000..7286c48 --- /dev/null +++ b/test/test-config-options.js @@ -0,0 +1,147 @@ +import JsonFileCRUD from "../lib/json-file-crud.js"; +import fs from "fs"; + +// Test setup +let passed = 0; +let total = 0; +let completed = 0; + +function test(description, testFn) { + total++; + testFn((err, success) => { + completed++; + + if (err || !success) { + console.log(`✗ ${description}: ${err?.message || 'failed'}`); + } else { + console.log(`✓ ${description}`); + passed++; + } + + // Check if all async tests are done + if (completed === total) { + console.log(`\n${passed}/${total} tests passed`); + process.exit(passed === total ? 0 : 1); + } + }); +} + +function cleanupFile(filePath) { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } +} + +// Test unique fields in create +test('unique fields prevent duplicates in create', (done) => { + const testFile = "./test-unique-create.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile, { + uniqueFields: ['email', 'username'] + }); + + crud.create({ name: "Ariel", email: "ariel@example.com", username: "ariel123" }, (err, item1) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + // Try to create another item with same email + crud.create({ name: "Yoni", email: "ariel@example.com", username: "yoni456" }, (err2, item2) => { + if (err2 && err2.message.includes("email")) { + // Try to create another item with same username + crud.create({ name: "Moshe", email: "moshe@example.com", username: "ariel123" }, (err3, item3) => { + cleanupFile(testFile); + if (err3 && err3.message.includes("username")) { + return done(null, true); + } + done(new Error(`should have failed for duplicate username, got: ${err3?.message || 'no error'}`)); + }); + } else { + cleanupFile(testFile); + done(new Error(`should have failed for duplicate email, got: ${err2?.message || 'no error'}`)); + } + }); + }); +}); + +// Test unique fields in update +test('unique fields prevent duplicates in update', (done) => { + const testFile = "./test-unique-update.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile, { + uniqueFields: ['email', 'username'] + }); + + // First create two items + crud.create({ name: "UpdateUser1", email: "updateuser1@example.com", username: "updateuser1" }, (err, item1) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + crud.create({ name: "UpdateUser2", email: "updateuser2@example.com", username: "updateuser2" }, (err2, item2) => { + if (err2) { + cleanupFile(testFile); + return done(err2); + } + + // Try to update item2 with item1's email + crud.update(item2.id, { email: "updateuser1@example.com" }, (err3, updatedItem) => { + cleanupFile(testFile); + if (err3 && err3.message.includes("email")) { + return done(null, true); + } + done(new Error('should have failed for duplicate email in update')); + }); + }); + }); +}); + +// Test autoId can be disabled +test('autoId can be disabled', (done) => { + const testFile = "./test-auto-id-disabled.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile, { autoId: false }); + + // Create item without ID (should work when autoId is disabled) + crud.create({ name: "NoAutoId" }, (err, item) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + // Should not have auto-generated ID + if (item.id === undefined) { + cleanupFile(testFile); + return done(null, true); + } + + cleanupFile(testFile); + done(new Error('should not have auto-generated ID when autoId is false')); + }); +}); + +// Test autoId enabled (default) +test('autoId works when enabled', (done) => { + const testFile = "./test-auto-id-enabled.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile, { autoId: true }); + + // Create item without ID (should get auto-generated ID) + crud.create({ name: "WithAutoId" }, (err, item) => { + cleanupFile(testFile); + if (err) return done(err); + + // Should have auto-generated ID + if (item.id === 1) { + return done(null, true); + } + + done(new Error(`expected auto-generated ID of 1, got: ${item.id}`)); + }); +}); diff --git a/test/test-delete.js b/test/test-delete.js index 2e827ee..92834d8 100644 --- a/test/test-delete.js +++ b/test/test-delete.js @@ -2,8 +2,6 @@ import JsonFileCRUD from "../lib/json-file-crud.js"; import fs from "fs"; // Test setup -const testFile = "./test-delete-data.json"; -const crud = new JsonFileCRUD(testFile); let passed = 0; let total = 0; let completed = 0; @@ -23,195 +21,253 @@ function test(description, testFn) { // Check if all async tests are done if (completed === total) { console.log(`\n${passed}/${total} tests passed`); - // Clean up test file - if (fs.existsSync(testFile)) { - fs.unlinkSync(testFile); - } process.exit(passed === total ? 0 : 1); } }); } -// Clean up and start fresh -if (fs.existsSync(testFile)) { - fs.unlinkSync(testFile); +function cleanupFile(filePath) { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } } -// Setup test data -const setupData = () => { - return new Promise((resolve, reject) => { - crud.create({ id: 1, name: "Ariel", age: 25, city: "Tel Aviv" }, (err) => { - if (err) return reject(err); - crud.create({ id: 2, name: "Yoni", age: 30, city: "Jerusalem" }, (err) => { - if (err) return reject(err); - crud.create({ id: 3, name: "Moshe", age: 35, city: "Haifa" }, (err) => { - if (err) return reject(err); - resolve(); - }); - }); +// Test basic delete +test("basic delete works", (done) => { + const testFile = "./test-delete-basic.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); + + // Create test data + crud.create({ id: 1, name: "Ariel", age: 25, city: "Tel Aviv" }, (err, item) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + // Delete the item + crud.delete(1, (err2, deletedItem) => { + cleanupFile(testFile); + if (err2) return done(err2); + if (deletedItem && deletedItem.name === "Ariel") { + return done(null, true); + } + done(new Error("delete failed")); }); }); -}; - -// Wait for setup, then run tests -setupData() - .then(() => { - runSequentialTests(); - }) - .catch((err) => { - console.log("Setup failed:", err.message); - process.exit(1); +}); + +// Test delete and verify removal +test("delete and verify removal works", (done) => { + const testFile = "./test-delete-verify.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); + + // Create test data + crud.create({ id: 2, name: "Yoni", age: 30, city: "Jerusalem" }, (err, item) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + // Delete the item + crud.delete(2, (err2, deletedItem) => { + if (err2) { + cleanupFile(testFile); + return done(err2); + } + + // Verify item was deleted + crud.findById(2, (err3, foundItem) => { + cleanupFile(testFile); + if (err3 && err3.message.includes("not found")) { + return done(null, true); + } + done(new Error("item should not be found after deletion")); + }); + }); }); +}); -function runSequentialTests() { - // Test 1: Basic delete - crud.delete(1, (err, deletedItem) => { +// Test count after deletions +test("count after deletions works", (done) => { + const testFile = "./test-delete-count.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); + + // Create test data + crud.create({ name: "Item1" }, (err, item1) => { if (err) { - console.log("✗ basic delete works:", err.message); - process.exit(1); + cleanupFile(testFile); + return done(err); } - if (deletedItem && deletedItem.name === "Ariel") { - console.log("✓ basic delete works"); + crud.create({ name: "Item2" }, (err2, item2) => { + if (err2) { + cleanupFile(testFile); + return done(err2); + } - // Test 2: Delete and verify removal - crud.delete(2, (err, deletedItem) => { - if (err) { - console.log("✗ delete and verify removal works:", err.message); - process.exit(1); + // Delete one item + crud.delete(item1.id, (err3, deletedItem) => { + if (err3) { + cleanupFile(testFile); + return done(err3); } - // Verify item was deleted - crud.findById(2, (err2, foundItem) => { - if (err2 && err2.message.includes("not found")) { - console.log("✓ delete and verify removal works"); - - // Test 3: Count after deletions - crud.count((err, count) => { - if (err) { - console.log("✗ count after deletions works:", err.message); - process.exit(1); - } - - if (count === 1) { - console.log("✓ count after deletions works"); - - // Test 4: Delete non-existent item - crud.delete(999, (err, deletedItem) => { - if (err && err.message.includes("not found")) { - console.log("✓ delete non-existent item fails"); - - // Test 5: Delete last remaining item - crud.delete(3, (err, deletedItem) => { - if (err) { - console.log("✗ delete last remaining item works:", err.message); - process.exit(1); - } - - // Verify no items left - crud.count((err2, count) => { - if (err2) { - console.log("✗ delete last remaining item works:", err2.message); - process.exit(1); - } - - if (count === 0) { - console.log("✓ delete last remaining item works"); - - // Test 6: Concurrent deletes - testConcurrentDeletes(); - } else { - console.log(`✗ delete last remaining item works: expected 0 items, got ${count}`); - process.exit(1); - } - }); - }); - } else { - console.log("✗ delete non-existent item fails: should have failed"); - process.exit(1); - } - }); - } else { - console.log(`✗ count after deletions works: expected 1 item, got ${count}`); - process.exit(1); - } - }); - } else { - console.log("✗ delete and verify removal works: item was not deleted"); - process.exit(1); + // Count remaining items + crud.count((err4, count) => { + cleanupFile(testFile); + if (err4) return done(err4); + if (count === 1) { + return done(null, true); } + done(new Error(`expected 1 item, got ${count}`)); }); }); - } else { - console.log("✗ basic delete works: delete failed"); - process.exit(1); + }); + }); +}); + +// Test delete non-existent item +test("delete non-existent item fails", (done) => { + const testFile = "./test-delete-nonexistent.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); + + // Try to delete non-existent item + crud.delete(999, (err, deletedItem) => { + cleanupFile(testFile); + if (err && err.message.includes("not found")) { + return done(null, true); } + done(new Error("should have failed for non-existent item")); }); -} +}); + +// Test delete last remaining item +test("delete last remaining item works", (done) => { + const testFile = "./test-delete-last.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); -function testConcurrentDeletes() { - // First add some items to delete - crud.create({ id: 10, name: "Test1" }, (err) => { + // Create one item + crud.create({ name: "LastItem" }, (err, item) => { if (err) { - console.log("✗ concurrent deletes work with queue:", err.message); - process.exit(1); + cleanupFile(testFile); + return done(err); } - crud.create({ id: 11, name: "Test2" }, (err) => { - if (err) { - console.log("✗ concurrent deletes work with queue:", err.message); - process.exit(1); + // Delete it + crud.delete(item.id, (err2, deletedItem) => { + if (err2) { + cleanupFile(testFile); + return done(err2); } - let completedDeletes = 0; - const totalDeletes = 2; - - // Delete multiple items at the same time - crud.delete(10, (err, item) => { - if (err) { - console.log("✗ concurrent deletes work with queue:", err.message); - process.exit(1); + // Check count is zero + crud.count((err3, count) => { + cleanupFile(testFile); + if (err3) return done(err3); + if (count === 0) { + return done(null, true); } - completedDeletes++; - checkCompletion(); + done(new Error(`expected 0 items, got ${count}`)); }); + }); + }); +}); - crud.delete(11, (err, item) => { - if (err) { - console.log("✗ concurrent deletes work with queue:", err.message); - process.exit(1); - } - completedDeletes++; - checkCompletion(); - }); +// Test concurrent deletes +test("concurrent deletes work with queue", (done) => { + const testFile = "./test-delete-concurrent.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); + + // Create multiple items + const items = []; + let createCount = 0; - function checkCompletion() { - if (completedDeletes === totalDeletes) { - // Verify both items were deleted - crud.findById(10, (err, item1) => { - if (err && err.message.includes("not found")) { - crud.findById(11, (err2, item2) => { - if (err2 && err2.message.includes("not found")) { - console.log("✓ concurrent deletes work with queue"); - console.log("\n6/6 tests passed"); - - // Final cleanup - if (fs.existsSync(testFile)) { - fs.unlinkSync(testFile); - } - process.exit(0); - } else { - console.log("✗ concurrent deletes work with queue: second item not deleted"); - process.exit(1); + for (let i = 1; i <= 3; i++) { + crud.create({ name: `Item${i}` }, (err, item) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + items.push(item); + createCount++; + + if (createCount === 3) { + // Now delete all concurrently + let deleteCount = 0; + + items.forEach((item) => { + crud.delete(item.id, (err2, deletedItem) => { + if (err2) { + cleanupFile(testFile); + return done(err2); + } + + deleteCount++; + if (deleteCount === 3) { + // Check final count + crud.count((err3, count) => { + cleanupFile(testFile); + if (err3) return done(err3); + if (count === 0) { + return done(null, true); } + done(new Error(`expected 0 items, got ${count}`)); }); - } else { - console.log("✗ concurrent deletes work with queue: first item not deleted"); - process.exit(1); } }); - } + }); } }); + } +}); + +// Test deleteAll +test("deleteAll removes all items", (done) => { + const testFile = "./test-delete-all.json"; + cleanupFile(testFile); + + const crud = new JsonFileCRUD(testFile); + + // Create some items first + crud.create({ name: "DeleteAllTest1" }, (err, item1) => { + if (err) { + cleanupFile(testFile); + return done(err); + } + + crud.create({ name: "DeleteAllTest2" }, (err2, item2) => { + if (err2) { + cleanupFile(testFile); + return done(err2); + } + + // Now delete all + crud.deleteAll((err3) => { + if (err3) { + cleanupFile(testFile); + return done(err3); + } + + crud.count((err4, count) => { + cleanupFile(testFile); + if (err4) return done(err4); + if (count === 0) return done(null, true); + done(new Error(`expected 0 items, got ${count}`)); + }); + }); + }); }); -} +});