diff --git a/lib/json-file-crud.js b/lib/json-file-crud.js index 6247f7b..d2fe1ce 100644 --- a/lib/json-file-crud.js +++ b/lib/json-file-crud.js @@ -4,29 +4,112 @@ * @author REL */ +import fs from "fs"; +import path from "path"; + class JsonFileCRUD { - constructor(filePath) { - if (!filePath) { - throw new Error('File path is required'); - } - this.filePath = filePath; + constructor(filePath, options = {}) { + if (!filePath) { + throw new Error("File path is required"); } + this.filePath = path.resolve(filePath); + this.idField = options.idField || "id"; + } - create(data, callback) { - // TODO: implement create - } + //#region CREATE - read(id, callback) { - // TODO: implement read - } + create(data, callback) { + // TODO: implement create + } - update(id, data, callback) { - // TODO: implement update - } + //#endregion CREATE - delete(id, callback) { - // TODO: implement delete - } + //#region READ + + /** + * Reads all items from the file + * @param {Function} callback - Called with (error, items) + */ + readAll(callback) { + fs.readFile(this.filePath, "utf-8", (err, content) => { + if (err && err.code !== "ENOENT") { + // File system error other than "file not found" + callback(err); + } else { + // File doesn't exist or read successfully - parse as JSON array + try { + const items = content ? JSON.parse(content) : []; + if (!Array.isArray(items)) { + throw new Error("File content is not a JSON array"); + } + callback(null, items); + } catch (error) { + callback(error); + } + } + }); + } + + /** + * Finds an item by ID + * @param {any} id - The ID of the item to find + * @param {Function} callback - Called with (error, item) + */ + findById(id, callback) { + this.readAll((err, items) => { + if (err) return callback(err); + + const item = items.find((item) => item[this.idField] === id); + if (!item) { + return callback(new Error(`Item with ${this.idField} ${id} not found`)); + } + + callback(null, item); + }); + } + + /** + * Finds items by filter function + * @param {Function} filterFn - Function that returns true for matching items + * @param {Function} callback - Called with (error, items) + */ + findBy(filterFn, callback) { + this.readAll((err, items) => { + if (err) return callback(err); + + const matchingItems = items.filter(filterFn); + callback(null, matchingItems); + }); + } + + /** + * Counts total items + * @param {Function} callback - Called with (error, count) + */ + count(callback) { + this.readAll((err, items) => { + if (err) return callback(err); + callback(null, items.length); + }); + } + + //#endregion READ + + //#region UPDATE + + update(id, data, callback) { + // TODO: implement update + } + + //#endregion UPDATE + + //#region DELETE + + delete(id, callback) { + // TODO: implement delete + } + + //#endregion DELETE } export default JsonFileCRUD; diff --git a/package.json b/package.json index d404f77..7f4b28e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "type": "module", "scripts": { "start": "node .", - "test": "node test/test.js", + "test": "node test/test-basic.js && node test/test-read.js", + "test:basic": "node test/test-basic.js", + "test:read": "node test/test-read.js", "prepublishOnly": "npm test" }, "keywords": [ diff --git a/test/test.js b/test/test-basic.js similarity index 89% rename from test/test.js rename to test/test-basic.js index 8713ede..a256ec9 100644 --- a/test/test.js +++ b/test/test-basic.js @@ -17,7 +17,7 @@ function test(description, testFn) { // Basic constructor test test('creates instance with file path', () => { const crud = new JsonFileCRUD('./test.json'); - if (!crud || crud.filePath !== './test.json') { + if (!crud || !crud.filePath.endsWith('test.json')) { throw new Error('failed to create instance'); } }); @@ -37,7 +37,7 @@ test('requires file path', () => { // Method availability test('has required methods', () => { const crud = new JsonFileCRUD('./test.json'); - ['create', 'read', 'update', 'delete'].forEach(method => { + ['create', 'findById', 'update', 'delete'].forEach(method => { if (typeof crud[method] !== 'function') { throw new Error(`missing ${method} method`); } diff --git a/test/test-read.js b/test/test-read.js new file mode 100644 index 0000000..7ff7026 --- /dev/null +++ b/test/test-read.js @@ -0,0 +1,82 @@ +import JsonFileCRUD from "../lib/json-file-crud.js"; +import fs from "fs"; + +// Test setup +const testFile = "./test-data.json"; +const testData = [ + { id: 1, name: "Ariel" }, + { id: 2, name: "Yoni" }, +]; + +// Create test file +fs.writeFileSync(testFile, JSON.stringify(testData)); + +const crud = new JsonFileCRUD(testFile); +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`); + fs.unlinkSync(testFile); // Clean up + process.exit(passed === total ? 0 : 1); + } + }); +} + +// Test reading all items +test('readAll works', (done) => { + crud.readAll((err, items) => { + if (err) return done(err); + if (items.length === 2) return done(null, true); + done(new Error('wrong count')); + }); +}); + +// Test reading single item +test('findById works', (done) => { + crud.findById(1, (err, item) => { + if (err) return done(err); + if (item.name === "Ariel") return done(null, true); + done(new Error('wrong data')); + }); +}); + +// Test findBy method +test('findBy works', (done) => { + crud.findBy(item => item.name === "Yoni", (err, items) => { + if (err) return done(err); + if (items.length === 1 && items[0].name === "Yoni") return done(null, true); + done(new Error('wrong data')); + }); +}); + +// Test error handling +test('findById error handling works', (done) => { + crud.findById(1000, (err, item) => { + if (err) return done(null, true); // Should fail + done(new Error('should have failed')); + }); +}); + +// Test count method +test('count works', (done) => { + crud.count((err, count) => { + if (err) return done(err); + if (count === 2) return done(null, true); + done(new Error('wrong count')); + }); +});