Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions lib/json-file-crud.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
4 changes: 2 additions & 2 deletions test/test.js → test/test-basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
Expand All @@ -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`);
}
Expand Down
82 changes: 82 additions & 0 deletions test/test-read.js
Original file line number Diff line number Diff line change
@@ -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'));
});
});